关于这篇文章的背景
之前了解到的事件代理不多, 就像是一个 dom 将事件委托给另一个 dom, 又叫事件委托. 后来做了个题目, 要实现一个类似 jQuery 的事件委托方法, 然后认真的了解了一下. 然后专注于实现, 其实并没有去看 jQuery 的源码, hhh.
发布订阅模式大概是目前前端框架使用的一种最常见的设计模式了, 而我目前也只对发布订阅模式有一定了解, 其他的设计模式待后续学习整理.
关于事件委托实现
在 jQuery 中, 实现事件委托只需要下面这一行代码就可以搞定
$("#container").on('click', 'item', fn)
之前会觉得这么用理所当然, 所以并没有想到哪一天我会亲手去实现一个这样的功能, 现在机会来了.
题目给的很简单, 就是将子节点的事件绑到其父节点上, 比如下面这段 dom, 就是将 < li > 的事件绑定到 < ul > 上, 实现点击 < li > 时触发 < ul > 上的事件.
- <div id="container">
- <li class="item" id="item1">
- <span id="btn1">hello</span>
- </li>
- <li class="item">
- <span>world</span>
- </li>
- </div>
题目的 JS 部分是这样的
- !function(root, doc){
- class Delegator {
- constructor (selector/* root 选择器 */) {
- // TODO
- }
- on (event/* 绑定事件 */, selector/* 触发事件节点对应选择器 */, fn/* 触发函数 */) {
- // TODO
- }
- destroy () {
- // TODO
- }
- }
- }(Windows,document)
然后实现一个功能类似上面 jQuery 的事件委托, 就像下面这样的
- var delegator = new Delegator('#container');
- delegator.on('click', 'li.item', fnli)
忘记一开始想的是什么方法了, 反正写到一半的时候突然想起了订阅发布模式 (因为刚好那几天在看发布订阅模式), 然后开始撸代码.
分解上面的方法, 其实可以看作是一个监听器
delegator.addEventListener('click', delegatorFn)
只不过这个 delegatorFn 有点特殊, 它需要遍历这个所有委托'click'事件给 delegator 的子节点, 并执行他们的委托 fnli
首先, 我们把所有的委托当作是一个订阅事件, 只不过这个事件里包含了委托者. 在 Delegator 对象的原型里增加一个属性 eventObj, 里面存放订阅'click'事件的所有的委托者和委托事件, 结构差不多是这样的
- this.eventObj.click=[{
- selecter:'li.item',
- callback:fnli
- }]
那么整个 on 的方法其实就是委托者 selector 订阅事件并委托给调 on 的被委托者
- on (event/* 绑定事件 */, selector/* 触发事件节点对应选择器 */, fn/* 触发函数 */) {
- // TODO
- if(!this.eventsObj[event]){
- this.eventsObj[event] = [];
- }
- this.eventsObj[event].push({
- selector,
- callback: fn
- })
- // 这里委托事件给 this.root, 当被委托者坚挺到 event 事件时, 会触发委托函数 delegatorFn
- this.root.addEventListener(event, delegatorFn);
- // 因为 on 可以链式调用, 所以这里需要返回
- return this;
- }
现在需要实现 delegatorFn 方法了, 其实也很简单, 主要是遍历事件经过的所有 dom 中哪个 selector 订阅了它, 它就执行对应节点携带的 fn.
- delegatorFn = (e) => {
- let target = e.target;// 触发事件的 selector
- let currentTarget = e.currentTarget; // 被委托者
- // 判断触发事件的节点及它冒泡经过的节点是否是被委托者, 若是, 则表示不再有委托者, 无需遍历
- while(target !== currentTarget){
- this.eventsObj[e.type].forEach(item => {
- // 查找订阅事件者
- if(target.matches(item.selector)){
- // 执行订阅事件者携带的函数
- item.callback.call(target,e);
- }
- });
- // 往上冒泡
- target = target.parentNode;
- }
- }
以上, 就是通过发布订阅实现的事件委托的核心部分, 主要涉及的还有事件的冒泡.
总结
主要涉及知识点:
链式调用: 在 on 方法里返回 this
事件冒泡: 对 e.target 进行往上查找并执行触发事件的回调
事件订阅
来源: https://juejin.im/post/5bf9472a518825251054070a