JS使用运算符实现事件委托
今天看到一个有意思的小项目esdelegates,里面写了一个demo使用 -=
/ +=
这样的运算符来实现事件的委托和取消
下面分析下代码的实现思路
# 完整源码
const delegates = (function() {
const globalDelegates = [()=>{}]
Function.prototype.valueOf = function () {
if (this.globalIndex !== undefined) return this.globalIndex
const index = globalDelegates.length;
Object.defineProperty(this, 'globalIndex', {
get() {
return index
}
})
globalDelegates.push(this)
return index
}
return function delegates() {
const handlers = {}
const callers = {}
return new Proxy(
{}, {
get(target, p) {
handlers[p] ??= []
return callers[p] ??= function (...args) {
handlers[p]?.forEach(x => x(...args))
}
},
set(target, p, newValue) {
const event = handlers[p] ??= [];
switch (typeof newValue) {
case 'function':
event.push(newValue)
return true;
case 'number':
const eventIndex = callers[p].valueOf();
let index = Math.abs(newValue - eventIndex);
const delegate = globalDelegates[index];
if (delegate == null) return false;
if (newValue > eventIndex) {
handlers[p].push(delegate)
return true;
}
else {
handlers[p].splice(handlers[p].findIndex(x => x === delegate), 1)
return true;
}
}
return false;
}
})
}
})()
# 实现思路和知识点
# valueOf
看一下mdn上对于valueOf
的解释
JavaScript 调用 valueOf 方法来将对象转换成基本类型值。强制数字类型转换强制基本类型转换优先会调用该方法
例如:
Function.prototype.valueOf = function () {
return 10;
};
const demo = () => {}
console.log(demo + 3);
// Expected output: 13
从例子中可以看出来,当函数参与到隐式转换时会使用基于valueOf
返回的基本数据类型,使得我们通过重写方法也能使函数参与到运算符运算中
# Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法:
new Proxy(target, handler)
在handle中可以定义诸多方法,在这里只用到了get
和set
。
handler.get()
方法用于拦截对象的读取属性操作。handler.set()
方法是设置属性值操作的捕获器
# 实现思路
globalDelegates
,这是一个全局的函数栈,所有的函数在valueOf
被重写后都会被添加到其中并添加唯一globalIndex
重写的valueOf
,当函数参与运算时返回已有globalIndex
,如果是函数第一次读取则使用defineProperty
设置globalIndex
字段,这个新设置的值是globalDelegates
当前长度,这样可以保证每个字段的值与索引一致。
delegates
实例返回的是Proxy
对象,其中设置了handlers
,callers
。handlers
的作用是保存实例对象下某字段添加的所有函数,callers
中则是在读取函数的时候将参数映射到对应handlers
字段并执行
# 流程示例
这样说还是有些抽象,下面以一个代码流程拆解作为说明
const demo = delegates();
// 示例化delegates
const handle = () => {
console.log('this is handle');
};
// 声明操作函数
demo.test += handle;
// 读取test字段,触发proxy.get在handlers中创建test字段的函数栈,callers中则会创建映射执行handlers的匿名函数
// 创建的匿名函数被读取,返回索引1,被压入globalDelegates
// handle函数被读取,返回索引2,被压入globalDelegates
// 运算符操作,两个索引操作结果为3,赋值给test字段,触发proxy.set,将实际运行的函数压入handlers
demo.test();
// 读取test字段,触发proxy.get。执行callers,callers执行handles对应字段下的所有函数
// 输出打印this is handle
# 关于 +=
和 -=
由上述流程不难发现,只要是进行了运算赋值,则在globalDelegates
中压入两个函数,一个是被赋值的匿名caller
,一个是赋值的实际函数handler
且一前一后。所以在proxy.set
时两索引相加必为正数,相减必为大数减小数为负数。当发现proxy.set
结果为负时从handlers中删去对应的函数,实现取消事件依赖
# 问题
这个事件委托的事件是完全依赖globalDelegates
中的顺序关系,所以所有的函数都会保存其中无法删除。在进行稍微复杂的依赖管理时也会出现顺序的错乱导致出错。当然这个无伤大雅,毕竟只是个玩具,实现效果还是蛮有意思