一个小需求
事情的起因,是昨天有一个新的需求被提出。
需求是要实现,让我们自己定制的弹出层,具备按下 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:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。