原生js实现自定义事件

637 阅读2分钟

事件是JavaScript和浏览器交互的主要途径。事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。 观察者模式:由两类对象组成,主体和观察者,主体负责发布事件,观察者通过订阅这些事件来观察主体。该模式的一个重要概念是主体并不知道观察者的任何事情,也就是说即使观察者不存在,主体也可以独自存在并正常运作。而观察者知道主体,并能注册事件的回调函数。

其实,“订阅”就是把处理程序交给“主体”;“发布事件”就是主体“调用”处理程序(上文的观察者)。

比如,主体 class A中有一个function fireEvent(){},处理程序是function handle1(){},则“发布事件”就是:

class A {  
    fireEvent() {  
        handle1()  
    }  
}  

“订阅”就是通过addHandles将处理程序handle1交给主体:

class A {  
    constructor() {  
        this.handles = {}  
    }  
    addHandles(handle) {  
        if (this.handles && this.handles.length) {  
            this.handles.push(handle)  
        }
    }  
    fireEvent() {  
        // 循环调用,所以DOM.addEventListener对于一个click可以添加多个处理程序  
        for(let handle of this.handles) {  
            handle()  
        }  
    }  
}  

还需要有个解除处理程序function removeHandle(handle) {//略}

所以一个主体(自定义事件),至少要有如上三个function。

当然,事件是有不同类型的,所以“订阅”addHandles可以改为:

addHandles(type, handle) {  
    if (!(this.handles[type] && this.handles[type].length)) {  
        this.handles[type] = []  
    }  
    this.handles[type].push(handle)  
}

而“发布事件”为:

fireEvent(type) {  
    if (this.handles && this.handles[type] && this.handles[type].length) {  
        for(let handle of this.handles[type]) {  
            handle()  
        }  
    }  
}  

通过继承来使用它:

class MyDom extends A {}  
let fakeDom = new MyDom()  
fakeDom.addHandles('myclick', function() {  
    console.log('watching myclick')  
})  

当某种情况下触发了“myclick”时,比如在class B的某个函数中:

function responseToEvent_MYCLICK() {  
    fakeDom.fireEvent('myclick')  
}  

自定义事件没有什么实用意义,但对理解浏览器运行机制有很大帮助。

所以额外的,想从底层机器运行到上层Javascript编程的视角描述整个过程:

  • 我们改下名字:A=>EventTarget(浏览器事件对象),B=>Window(浏览器窗口对象)
    然后是对MyDom的修改:(以下代码仅示意,并非真实实现)
    class MyDom extends EventTarget {
        constructor() {
            super()
            // 当创建DOM元素时,该元素便被注册进Window中了
            // 触发事件时window就可以找到该实例,调用fireEvent了
            Window.addTarget(this)
        }
    }
    
  1. 首先,鼠标单击行为产生信号脉冲,USB口接收信号后,将发送中断信号给CPU,CPU将信号通知给相应进程,此时是浏览器的进程。
  2. 其中Browser进程负责监听和计算各个事件的触发和触发相关的元素,它将接收CPU的信号,通知Render进程对相关元素进行修改,Render进程执行一系列程序,其中便运行到Window的responseToEvent_MYCLICK。
  3. 于是,之前通过window.addTarget注册的各元素的fireEvent就可以被调用了,即:
    function responseToEvent_MYCLICK() {  
        this.doms.map(dom => {  
            // fireEvent内部判空了,不用在乎dom有没有监听myclick  
            dom.fireEvent('myclick')  
        })  
    }  
    
    注意,这里使用的是“通知”而非“调用”,是因为进程间是通过共有一内存来互相“调用”的。