最近面试经常拿这个学校选择器作为例子来讲自己的JS学习过程,有位工程师哥哥直接打开这里“注册”里的第二步,就是我这个功能最初应用的地方。跟他讨论实现模态时事件的unbind这种方式是不是不好时,他给了我一点启发,于是我回过头来重新思考。
问题来源
假设页面上有这么三行元素
1 | <div> |
想要对每行都“选择学校”,并将选中的值填回该行中的input
元素中,最早代码是酱紫的。
1 | $('.choose-school').click(function(event){ |
当点一个元素时没问题,但是点了两个三个元素后,就会发现每次“选择学校”后都会一起刷新。原因就在于这三个元素都把各自的回调加绑到了schoolChosen
事件中,而schoolBox
的schoolChosen
被触发时,它会依次调用绑到它身上的handlers。
在SchoolBox内部的实现中,使用一个map来记录所有的事件回调,key是事件名称,而value是监听该事件的回调函数的数组。
1 | SchoolBox.prototype = { |
因此以上实现代码是有弊端的,页面上多个元素共享同一个模态对话框时,无法正确地响应事件。
在上一篇一步步做组件-学校选择器(7)中使用了一种暴力的解决办法,每次click时都对SchoolBox对象unbind掉所有的事件,然后重新监听,以保证每次打开SchoolBox时handlers map中该事件类型的回调函数只有唯一的一个。很明显这种做法不是很妥。
从监听上入手
由于上面的代码在监听时只绑定事件名称,不区分监听者元素,因此很容易想到在on
监听时把当前操作的元素ID连同回调函数一起传入。这样的话,SchoolBox内部的handlers的map格式要稍微改下,即{ eventType1: { sourceId1: function(){} } }
这样的格式。
1 | SchoolBox.prototype = { |
这里on
时由type + sourceId
来确定handler,注意这里fire
时根据type + this.curSourceId
来取出相应的handler回调函数。而这个this.curSourceId
需要在SchoolBox每次被打开的时候更新,每次被关闭的时候清空其值。
1 | SchoolBox.prototype.show = function(sourceId){ |
最后应用层的监听代码就像这样
1 | $('.choose-school').click(function(){ |
虽然每次show时都要传入当前元素的ID,但是肯定比每次click都先unbind全部,再重新bind要好。
看起来是没错
像上面的应用层代码,在监听时判断的话,每个监听者元素被click时,都会重新去执行schoolBox的on
方法,反复地生成同一个匿名的回调函数并绑定到同一个位置(handlers[type][id]
)。显然上面的代码能够改成这样。
1 | var $source; |
引入了一个全局的$source
变量去记录当前click的元素(以使回调中能够正确操作相应的元素),虽然这样很矬,但是至少避免了每次click时都去执行schoolBox的on
方法,而且在循环中使用了callback
函数的引用,避免了重复生成相同的匿名函数。总的来说,虽然长得丑,但是效率上是有提高的。
反思
但是对于其中这小段代码
1 | $('.choose-school').each(function(){ |
我开始怀疑自己,为什么在一个循环中反复去为不同的元素ID监听同一个事件类型,更可笑的是使用同一个回调函数?于是我开始反思当初设计监听者回调函数的初衷。
最早在一步步做组件-学校选择器(2)中我是这样使用回调的。
1 | var schoolBox = new SchoolBox({ |
这样的缺点是,如果页面中有多个不同的元素都要对“学校选中”做出响应,那么这部分响应代码都得写在这个schoolClickCallback
这里。如果需要响应的那些元素来自页面的不同区域,负责完全不同的功能,这样把响应代码都揉在一个地方的话,会造成一定的耦合。
所以出于这样的考虑,我想到使用观察者模式(即自定义事件),不同的元素各自监听自己需要的事件,自己维护自己如何响应。
1 | $('.choose-school').each(function(){ |
而这段代码似乎与这个初衷有点走歪路了,首先它们确实是不同的元素,但也是相似的元素,而它们监听的事件类型也相同,对事件响应的方式也相似。如此“强行”为不同的元素各自监听,是不是有点生搬硬套的意思了?
在回调时判断
我从事件委托那里得到了启发,为何不为相似的元素统一监听一个事件,并且只有唯一一个回调函数。具体做法就是在fire
事件时加入“由哪个元素触发”,以避免对所有元素都做出响应。
1 | SchoolBox.prototype = { |
这里对on
不做改变,this.handlers
还是原来的{ eventType1: [ function(){} ]}
格式。在fire
内部,在handlers回调前,把当前的this.curSourceId
(当前打开SchoolBox的触发元素ID)也塞到回调参数data中。
这里同样要为SchoolBox的show
和hide
方法里加入curSourceId
的判断,代码同从监听上入手。
然后应用层具体的回调函数中只需要加入对sourceId
的判断。
1 | // 监听自定义事件 |
在回调时找到“打开SchoolBox”的触发元素,然后只对相应的元素做出响应。正如事件委托比“为所有子元素都绑定事件”要好一样,这里“在回调时判断”也比“在监听时判断”效率要高。
写在最后
这个系列的文章从第1篇到现在第8篇,前后跨度2个半月,而最早在项目中做这部分功能已经是半年前了。经过这段反复审视代码和写博客的过程,自己得到了不少的提高,把以前看书看别人博客所记下的一堆零碎的知识融合了起来。也正是这个项目和这个博客,在我找实习面试的时候帮我加了很多分。
我会继续坚持走下去,回头看看走过的路和踩过的坑。谢谢~~(本系列完结)