JavaScript is required
Blog About

JS使用运算符实现事件委托

2024/01/12
6 mins read
See this issue
# Javascript
Back

今天看到一个有意思的小项目esdelegates,里面写了一个demo使用 -= / += 这样的运算符来实现事件的委托和取消

image

下面分析下代码的实现思路

# 完整源码

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中可以定义诸多方法,在这里只用到了getset

handler.get() 方法用于拦截对象的读取属性操作。handler.set() 方法是设置属性值操作的捕获器

# 实现思路

globalDelegates,这是一个全局的函数栈,所有的函数在valueOf被重写后都会被添加到其中并添加唯一globalIndex

重写的valueOf,当函数参与运算时返回已有globalIndex,如果是函数第一次读取则使用defineProperty设置globalIndex字段,这个新设置的值是globalDelegates当前长度,这样可以保证每个字段的值与索引一致。

delegates实例返回的是Proxy对象,其中设置了handlerscallershandlers的作用是保存实例对象下某字段添加的所有函数,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中的顺序关系,所以所有的函数都会保存其中无法删除。在进行稍微复杂的依赖管理时也会出现顺序的错乱导致出错。当然这个无伤大雅,毕竟只是个玩具,实现效果还是蛮有意思