一个小需求

事情的起因,是昨天有一个新的需求被提出。

需求是要实现,让我们自己定制的弹出层,具备按下 ESC 也能退出的功能。我把任务交给了同组的小伙伴S去实现。(这个项目用到了vue技术栈,以及饿了么的UI框架。)

我开完会回来,发现他还在处理那个功能,但好像遇到了什么瓶颈。于是,我就问他,卡在了什么地方。

小伙伴S说,他百度了不少资料,还查了官方文档,并且尝试其中的办法,但就是无法触发按下 ESC 的回调方法,很是郁闷。我看了他的代码,他的写法是这样的:

<div class="custom-modal" @keydown.27="handlePressEscape">
    ...
</div>
...
handlePressEscape () {
    console.log('press escape!');
},
...

他的想法不错,因为是自定义的弹出层,所以就想着把 keydown 事件,绑定在最外层的 div 上,让整个弹出层都能监听到。

他给我看了他查的资料,几乎都是在 input 上绑定 keydown 事件的例子,而 vue 的官方文档里也是类似的例子,实践后却陷入了瓶颈。但是他忽略了一个问题,keydown 事件,并非绑在任意一个标签上,都会起作用

一种思路

我没有直接把答案告诉他,而是给他提供了一个思路:在我们常用的 element-ui 的 el-dialog 组件里,有个属性叫做 close-on-press-escape,它的解释如下图:

从文档的解释,可以看出 el-dialog 在默认情况下,已经实现了我们需要解决的需求。所以,我让他看看 el-dialog 的源码,是如何实现的。

他一听要看源码,就露出了恐惧之情。

源码是所有框架和API的根基,因为比较复杂深邃,所以让人很抗拒。我自己也经历过这个阶段,所以非常理解他的心情,鼓励他一起做一次尝试。

查找源码

首先,我们在 node_modules 里,找到了 element-ui的文件夹,它大致长这个样子:

接着,我们找到了 packages 里的 dialog 文件夹,再从 index 入口,找到了组件 component.vue。可是,点进去找了半天,也只找到个 closeOnPressEscape 属性的定义,却没有实现的方法。

...
closeOnPressEscape: {
    type: Boolean,
    default: true
},
...

这么神奇么?只定义一个属性,就能实现一个事件的交互了?

感觉不太可能啊?!? 为了揭开迷雾,继续找。。。

仔细浏览了 component.vue 文件,发现在 script 里,引入下面 3 个文件:

import Popup from 'element-ui/src/utils/popup';
import Migrating from 'element-ui/src/mixins/migrating';
import emitter from 'element-ui/src/mixins/emitter';
...

在第一个引入的 Popup 中,竟然也发现了 closeOnPressEscape,感觉似乎找对方向了。

但令人沮丧的是,Popup 中同样只有 closeOnPressEscape 的属性定义,却没有实现。不过,它却引入了另一个辅助文件 PopupManager,再点进去找。

哇!终于找到了!它的实现,是这样的:

// handle `esc` key when the popup is shown
window.addEventListener('keydown', function(event) {
    if (event.keyCode === 27) {
        const topPopup = getTopPopup();

        if (topPopup && topPopup.closeOnPressEscape) {
            topPopup.handleClose
                ? topPopup.handleClose()
                : (topPopup.handleAction
                    ? topPopup.handleAction('cancel')
                    : topPopup.close());
        }
    }
});

原来,是在 window 上添加了事件监听 keydown,当监测到是 ESC 的 keyCode 时,则执行相关操作。

模仿源码

ok,现在已经知晓了原理,那就按照我们的实际需求,模仿改造一下:

...
props: {
    ...
    closeOnPressEscape: {
        type: Boolean,
        default: true
    }
},
...
mounted () {
    window.addEventListener('keydown', this.handlePressEscape);
},

destroyed () {
    window.removeEventListener('keydown', this.handlePressEscape);
},

methods: {
    ...
    handlePressEscape (event) {
        if (this.closeOnPressEscape && event.keyCode === 27) {
            this.handleClose();
        }
    }
}

在上述实现中,有2个需要注意的点:

  • 代码方面,在 mounted 中,给 window 添加事件监听之后,要记得在 destroyed 时,去除监听。
  • 业务方面,这是一个我们定制的通用的弹出层组件,所以在 props 中定义了一个 closeOnPressEscape 属性,以方便在某些业务场景下,不需要按 ESC 就退出这个功能的时候,直接设置它为 false 即可。

到此为止,整个事件画上了圆满的句号。

源码真有那么可怕吗?

源码一词,乍一听就是神秘、高大上、吊炸天的代名词,让很多的前端同学闻风丧胆。回想当初,我也曾一度对它有一股深深的恐惧。

源码真的这么可怕吗?

从以上的事例中可以看出,其实并没有。例子中的element-ui源码并不复杂,我和小伙伴S一起看源码时,他也大概都能看得明白。最后因为弄懂了背后的原理,进行简单应用,比较轻松地就解决了问题。

对于源码的恐惧,让我们渐渐思维固化,自己告诉自己不要去碰源码,时间长了就遗忘了还有这样一条路可走。

面试中的应用

关于对源代码的考察,我也会经常应用在面试中。在面试中,如果候选人给我的感觉不错,我的惯用伎俩是问下面这个问题:

这个问题,意在考量候选人的思维方式和解决问题的能力,以及把他思考的过程阐述清楚的表达能力。这三种能力,往往比使用过某些框架的经验,更让我看中。

这道题的回答思路,其实就是可以通过挖掘源码,去实现功能。另外也可以通过海量地查找资料,发现原生js的实现方式,但这条路没有直接挖掘源码来得快。在遇到实际的业务问题的时候,参考源码的原理和写法,往往能更快地解决问题。

这是我自己对这道题目,给出的答案。

一点点思考

昨天的案例,引发了我的一连串思考:

现代框架的确降低了前端入门的门槛,提高了开发效率。

但是,在使用这些框架的过程中,我们到底学到了什么?

脱离了框架和它的API,我们脑海中还剩下些什么?

以至于,当下一个更新更棒的框架出现的时候,我们是否能够用已经学到的知识,帮助自己更迅速地上手?

知其然,并知其所以然,学习所有的知识都应当有这种探索精神。我们不仅仅是代码的搬运工。领悟这些深层次的原理,比起仅仅熟练地掌握一门框架,要实用得多。

PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。

03-05 17:17