JS事件触发之后发生了什么

499 阅读4分钟

前言

JS事件触发之后发生了什么?要回答这个问题必须搞清楚事件模型,JS中的事件模型分为三个阶段:捕获、目标、冒泡,先从Dom树根节点向下捕获,到达目标之后执行回调函数向上冒泡,直到根节点;如何证明是先捕获然后目标阶段最后冒泡的呢?看一下下面这个案例:

从根节点到目标节点绑定多个事件模拟事件模型

先看如下代码,打开控制台然后立即点击hello world「不要点击多次,只点击一次就行」:

从根节点到目标节点绑定了多个事件,每个事件中的代码都是同步阻塞性代码,先不要问为什么这样写,先来看看结果,结果就是点击之后,前面10ms打印了window捕获事件,然后打印了10msapp捕获事件,然后是app click事件,最后才是window click事件,这也佐证了上面的说法基本上是对的,现在看的是整个事件执行的过程,如果在同一个元素上绑定多个同步阻塞事件,会怎么样?

同一个元素绑定多个同步阻塞事件

直接把上面的代码换成这样:

addClickEvent({
    htmlElement:app,
    printTime:10,
    logString:"app click1",
    isCapture:false
})

addClickEvent({
    htmlElement:app,
    printTime:10,
    logString:"app click",
    isCapture:false
})

可以看到同一个元素上先后绑定的事件是有执行顺序的,先绑定的先执行,如果没有同步阻塞代码,那么同一个元素上绑定的事件肯定是紧随其后地执行,为什么同步代码可以阻塞事件的执行?这就需要详细地研究一下事件执行的过程,它不只是上面提到的“捕获”、“目标”、“冒泡”,还漏掉了一个过程:到达目标之后回调函数怎么执行的?

事件回调函数的处理

浏览器是多进程架构,它主要有浏览器主进程、渲染进程、插件进程、GPU进程、网络进程,每新增一个Tab页默认会新增一个渲染进程,可以打开浏览器的任务管理器查看这些进程;浏览器内核一般就指的是渲染进程,在渲染进程中又包含多个常驻线程:JS引擎线程、http请求线程、GUI线程、事件触发线程、定时器触发线程;

v2-b61cab529fa31301bde290813b4587fc_720w.png

当一个事件被触发时事件触发线程会把回调函数添加到“任务队列”的队尾,等待JS引擎的处理,而JS引擎只有到空闲的时候才会从“任务队列”中取出一个任务执行;而上面的代码中每一个事件处理函数都把线程阻塞了,也就是让JS引擎线程没有空闲处理其他任务,因此只有等到同步的循环完成之后才执行下一个事件处理程序;

任务队列

任务队列也可以叫做消息队列,英文叫做Message Queue,它的数据结构类似于队列;特点就是:FIFO「先进先出」;前端接触MQ比较少,一般都是后端的分布式系统会接触到MQ,MQ中有生产者、消费者的概念;一个系统充当生产者,不断地向消息队列添加消息;另一个系统充当消费者,不断地从消息队列中取出消息然后执行对应逻辑,这是一种异步的消费数据的逻辑;

再回到事件上来,事件触发线程就是生产者,JS引擎线程就是消费者,所以从事件触发到事件处理程序执行这个过程其实也是异步的;如果JS引擎线程被阻塞那么即使事件触发,事件回调函数也不会执行;因此,尽量不要在事件处理函数中执行CPU密集型任务,它会阻塞下一个事件处理函数;

总结

首先JS引擎解析到事件绑定之后,会告知事件触发线程监听事件;当用户触发事件,事件触发线程会将所有的事件回调添加到消息队列中,当JS引擎线程空闲之后会从消息队列中取出一条执行,直到执行完所有事件回调函数;

因此,不应该在事件回调中执行CPU密集型操作,否则会影响事件回调函数的执行。

参考