ps: 本文首发于公众号 话说前端
在目前的项目开发中,由于使用的 angular 1.x
来进行应用层面的开发,那么在处理事件的时候就不得不使用框架自身所带的 ng-click
指令来进行事件的绑定,使用 ng-click
进行绑定的话,对于长列表,交互事件多的地方来说,不是一个好的选择。因为会绑定很多的事件。于是就寻求以一种合适的事件代理的方式来尝试替代(这里只讨论 click
)。所以来做个总结
要完成这件事,不妨先回顾一下事件流的一些知识
如上图所示,当你点击页面上的元素的时候,事件会依次到达这三个阶段,它们分别是:
事件捕获阶段 -> 目标阶段 -> 冒泡阶段。
单拿出冒泡阶段来说,意味着当你点击了某一个具体的元素后,那么最后你是可以在document这个层级接收到事件的,这就构成了代理的基础。
接下来就要考虑的问题是:
- 我在根节点上的监听方法如何响应组件里面定义的回调函数
- 我们的写法如何与
ng-click
保持一致。 - 实现事件合成的冒泡与阻止冒泡
先来看正常的 ng-click
指令是如何使用的,如下:
<example
ng-click="test($event,item)"
></example>
所以只需要实现一个指令,接收一个函数即可(这里只讨论属性为函数的场景)。
回调函数的查找
const clickDirective = function () {
return {
restrict: 'A',
link(scope, element, attr) {
try {
if (!attr.mmClick) return;
const { fn, functionName, paramsArr } = service.parseParams(attr.mmClick, scope);
// eslint-disable-next-line
element[0][`rcs-${functionName}`] = { fn, paramsArr };
} catch (e) {
console.error(e);
}
},
};
};
如上,我们可以在指令的初始化中,将我们获取到的回调函数以及参数以特定名称的方式挂载到 dom
树上
这里以 rcs
字符开头,这样就能在根节点接收的事件中去寻找到这个回调函数并处理
function executeEvent(target, e) {
const keys = Object.keys(target);
for (let i = 0; i < keys.length; i++) {
if (keys[i].indexOf('rcs') === 0) {
const { fn, paramsArr } = target[keys[i]];
fn.call(this, ...paramsArr, { e });
}
// 处理是否需要冒泡
if (e.hasOwnProperty('stopBubble') && e.stopBubble) {
return;
}
}
if (target.parentNode !== null) {
return executeEvent(target.parentNode, e);
}
}
可以看到,上面的函数是一种递归的方式,逐级寻找回调函数,并执行它。这样就完成了函数的定义与执行部分。
合成事件的冒泡与阻止冒泡
与此同时,在一个dom层级上,可以在不同的层级绑定事件,这样在冒泡阶段,会依次执行。那么在如上的函数中也体现了这点。
如上的函数,会把事件对象通过参数的形式给到调用方(别问为啥在最后,因为要兼容已有函数定义),
fn.call(this, ...paramsArr, { e });
在回调函数里,只需要设置这个值即可:
$scope.showGroupCard = function (item, chatInfoType, { e }) {
e.stopBubble = true;
}
当这个值为 true
的时候,那么 executeEvent
就会终止执行,否则会直到 parentNode
为null
。
最后再来看看使用效果:
<example
mm-click="test(item)"
></example>
其实到这里,这个事件合成简易版就算完成了。虽然是利用的dom自身的层次性,但查找速度完全不用担心。
另外就是关于回调函数里面的 this 的问题,回调函数需要使用箭头函数来穿透才行,问题不大
函数参数解析
其实这里最难的部分在于指令的函数解析上,一个函数它的参数可以有如下几种(可能我还没罗列完):
- 简单的基本数据类型
- 变量(这个变量可能来自自己组件的作用域,来自父组件传递下来的作用域,也可能来自根作用域)
- 参数是一个函数,或者是一个递归函数
- 参数是一个对象,对象里面有变量
- 等等
所以基于此,再结合实际的业务场景,约定了如下:
- 指令的属性只能是一个函数,不接受其他值
- 只保留了当前作用域变量,当前变量的调用,舍弃了表达式等写法,
- 函数参数只是当前组件作用域及作用域链上的,如果需要使用表达式或者其他写法,就把它挂载到当前作用域上。
end
万物起于微忽,量变引起质变
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。