您好,欢迎访问一九零五行业门户网

理解JavaScript中的事件路由冒泡过程及委托代理机制

当我用纯css实现这个以后。我开始用javascript和样式类来完善功能。
然后,我有一些想法,我想使用delegated events (事件委托)但是我不想有任何依赖,插入任何库,包括jquery。我需要自己实现事件委托了。
我们先来看看事件委托到底是什么?他们是怎么工作的,怎么去实现这种机制。
好,它解决了什么问题?我们先看个简单的例子。
先假设我们有一组按钮,我一次点击一个按钮,然后我希望被点中的状态设为active。再次点击时取消active。
然后,我们可以写一些html:
<ul class="toolbar"> <li><button class="btn">pencil</button></li> <li><button class="btn">pen</button></li> <li><button class="btn">eraser</button></li> </ul>
我可以用一些标准的javascript事件处理上面的逻辑:
var buttons = document.queryselectorall(".toolbar .btn"); for(var i = 0; i < buttons.length; i++) { var button = buttons[i]; button.addeventlistener("click", function() { if(!button.classlist.contains("active")) button.classlist.add("active"); else button.classlist.remove("active"); }); }
看上去不错,但是它其实不能像你期望的那样工作。
闭包的陷阱如果你有一定的javascript开发经验,这个问题就很明显了。
对于外行来说button变量是被封闭的,每次都会找到对应的button……但是其实这里只有一个button;每次循环都会被重新分配。
第一个循环它指向第一个button,接下来是第二个。但当你点击时button变量永远只指向最后一个button元素,问题出在这。
我们需要的是一个稳定的作用域;让我们重构一下。
var buttons = document.queryselectorall(".toolbar button"); var createtoolbarbuttonhandler = function(button) { return function() { if(!button.classlist.contains("active")) button.classlist.add("active"); else button.classlist.remove("active"); }; }; for(var i = 0; i < buttons.length; i++) { buttons[i].addeventlistener("click", createtoolbarbuttonhandler(buttons[i])); }
注* 上面这段代码结构有点复杂,也可以简单直接地使用一个闭包,封闭保存当前的button变量,如下所示:
var buttons = document.queryselectorall(".toolbar .btn"); for(var i = 0; i < buttons.length; i++) { (function(button) { button.addeventlistener("click", function() { if(!button.classlist.contains("active")) button.classlist.add("active"); else button.classlist.remove("active"); }); })(buttons[i]) }
现在它能正常工作了。指向永远是正确的button
那么这个方案有什么问题?这个方案看上去还可以,然而我们确实可以做得更好。
首先我们创建了太多的处理函数。为每一个匹配的.toolbar button绑定了一个事件侦听和一个回调处理。假如只有三个按钮这种资源分配是可以忽略的。
然而,如果我们有1000个呢?
<ul class="toolbar"> <li><button id="button_0001">foo</button></li> <li><button id="button_0002">bar</button></li> // ... 997 more elements ... <li><button id="button_1000">baz</button></li> </ul>
它也不会崩溃,但是这并不是最佳的方案。我们分配了大量不必要的函数。让我们重构一下,仅附加一次,即仅绑定一个函数(function),去处理这种有可能的数千次调用。
相对于封闭button变量去存储当时我们点击的对象,我们可以使用event对象去获取当时点击的对象。
event对象有一些元数据,在多次绑定的种情况下,我们可以使用currenttarget获取当前绑定的对象,如上例的代码就可以改成:
var buttons = document.queryselectorall(".toolbar button"); var toolbarbuttonhandler = function(e) { var button = e.currenttarget; if(!button.classlist.contains("active")) button.classlist.add("active"); else button.classlist.remove("active"); }; for(var i = 0; i < buttons.length; i++) { button.addeventlistener("click", toolbarbuttonhandler); }
不错!不过这只是简化了单个函数,让它得更具可读性,然而它还是被绑定了多次。
但是,我们还可以做得更好。
让我们假设一下,我们在这个列表里动态地添加了一些按钮。然后我们还要为这些动态元素添加和移除事件绑定。然后我们还要持久化这些处理函数和当前上下文要用到的变量,这事听上去就不靠谱。
也许还有其他方法。
让我们先全面理解一下事件的工作原理,以及他们在dom里是怎样传递的。
事件的工作原理当用户点击一个元素时,一个事件就会被产生去通知用户当前的行为。事件在分发派遣时会有三个阶段:
捕获阶段: capturing
触发阶段: target
冒泡阶段: bubbling
这个事件起始从document之前然后一路向下找到当前事件点击到的对象。当事件达到点击到的对象之后,它会按原路返回(冒泡过程),直到退出整个dom树。
这里是一个html的例子:
<html> <body> <ul> <li id="li_1"><button id="button_1">button a</button></li> <li id="li_2"><button id="button_2">button b</button></li> <li id="li_3"><button id="button_3">button c</button></li> </ul> </body> </html>
当你单击button a时,事件经过的路径会向下面这样:
start | #document \ | html | | body } capture phase | ul | | li#li_1 / | button <-- target phase | li#li_1 \ | ul | | body } bubbling phase | html | v #document / end
注意,这意思着你可以在事件的经过路径上捕获到你单击所产生的事件,我们非常确定这个事件一定会经过他们的父元素ul元素。我们可以将我们的事件处理绑定到父元素上面,然后简化我们的解决方案,这个就叫事件的委托及代理(delegated events)。
注* 其实flash/silverlight/wpf开发的事件机制是非常近似的,这里有一张他们的事件流程图。 除了silverlight 3使用了旧版ie的仅有冒泡阶段的事件模型外,基本上也都有这三个阶段。(旧版ie和sl3的事件处理只有一个从触发对象冒泡到根对象的过程,可能是为了简化事件的处理机制。)
事件委托代理委托(代理)事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。
让我们看一个具体的例子,我们看看上文的那个工具栏的例子:
<ul class="toolbar"> <li><button class="btn">pencil</button></li> <li><button class="btn">pen</button></li> <li><button class="btn">eraser</button></li> </ul>
因为我们知道单击button元素会冒泡到ul.toolbar元素,让我们将事件处理放到这里试试。我们需要稍微调整一下:
var toolbar = document.queryselector(".toolbar"); toolbar.addeventlistener("click", function(e) { var button = e.target; if(!button.classlist.contains("active")) button.classlist.add("active"); else button.classlist.remove("active"); });
这样我们清理了大量的代码,再也没有循环了。注意我们使用了e.target代替了之前的e.currenttarget。这是因为我们在一个不同的层次上面进行了事件侦听。
e.target 是当前触发事件的对象,即用户真正单击到的对象。
e.currenttarget 是当前处理事件的对象,即事件绑定的对象。
在我们的例子中e.currenttarget就是ul.toolbar。
注* 其实不止事件机制,在整个ui构架上flex(不是flash) /silverlight /wpf /android的实现跟web也非常相似,都使用xml(html)实现模板及元素结构组织,style(css)实现显示样式及ui,脚本(as3,c#,java,js)实现控制。不过web相对其他平台更加开放,不过历史遗留问题也更多。但是几乎所有的平台都支持web标准,都内嵌有类似webview这样的内嵌web渲染机制,相对各大平台复杂的前端ui框架和学习曲线来说,使用web技术实现native app的前端ui是非常低成本的一项选择。
以上就是理解javascript中的事件路由冒泡过程及委托代理机制的内容。
其它类似信息

推荐信息