关于浏览器处理事件的问题?

当我们在dom上绑定一个click事件,浏览器做了什么事,事件处理程序被存在哪?如何抛出一个事件对象?会有一个事件轮询线程会接受到这个通知吗?如果接收到这个通知了,是如何通知js线程去执行处理程序?或者说通知js线程将回调扔到执行队列?如果js线程处于阻塞状态,回调还会被扔到执行队列么? 求大神们大概描述下整个过程,不甚感激
关注者
87
被浏览
4724
谢邀。
因为我不太确定我这个答案是完全正确的,只能勉力一答(还请各位做review),顺带一些跑题,所以先匿了,另外,我还看到R大关注问题了,先抛砖引玉再说。


答题之前,先引 @冯东先生之前的一段回答。

但凡这种「既是单线程又是异步」的语言有一个共同特点:它们是 event-driven 的。驱动它们的 event 来自一个异构的平台。这些语言的 top-level 不象 C 那样是 main,而是一组 event-handler。虽然所有 event-handler 都在同一个线程内执行,但是它们被调用的时机是由那个驱动平台决定的。而且设计要求每个 event-handler 要尽快结束。未做完的工作可以通知那个异构的驱动平台来完成。所以那个驱动平台可以有许多线程。
恰好,浏览器就是这种 event-driven 架构的软件。

事实上,ECMAScript并没有从语言上约定其异步的特性,我们所探讨的“异步”都是由执行引擎所赋予的。于Firefox,这个引擎是SpiderMonkey,于Node.js这个引擎是V8。而提供这个异步能力的机制,则是我们所谓的Event Loop——事件轮询,而本质上来说就是Reactor(反应堆)模型的一种延伸实现。所以像setTimeout,setInterval这样的函数,实际上并不是由语言本身所约定的,而是浏览器/执行引擎来实现,向JavaScript暴露的、提供的异步入口。

(上图描述了Node.js中异步任务的执行流程)


因此,异步与单线程并没有出现矛盾。而具体到浏览器端,每个跃然于我们屏幕之前的Tab页,都拥有一个JS执行线程,即
There is only one JavaScript thread per window.

正如上文提到的,页面上虽然只提供了一个JavaScript Call Stack用于执行代码,不过浏览器在内部还实现了一个或多个队列,借由事件轮询的机制来调度全部事件的处理,而且在一定程度上,Programmer有权access到这个内部的轮询中。其一,可以是Timer函数,其二,则可以是通过该题题主问到的DOM事件。

而即使是DOM事件的接口中也还有同步事件与异步事件的区别。
DOM的同步方法,比方说DOM.setAttribute,DOM.style等等,顾名思义,它们都会在当前JS的执行线程同步执行,也因此我们在使用这些方法,有时候会带来重排重绘的副作用。
而异步事件,比如DOM.addEventListener,则会将函数以类似"委托"的形式注册到浏览器内建的队列中,等到某个"事件"被触发后,则回Call之前注册的函数。流程类似下图所示:

按图中所示,题主的Click事件会经历完整的1->2->3->4->5的生命周期,而假设当我们的事件正处于在Stage:5的状态做密集执行,与此同时又触发了别的事件,e.g. Timer 或者Interval,则后续的事件将会持续Pending在Event Queue中,直到Click的回调中所有同步代码执行完毕,Event Loop选取下一个在队列顶部的任务,再次执行。


此外,如图所示,如果Interval在第二次触发时,上一次的回调仍未获得执行,则该次调用自动被抛弃。这也是为什么,红宝书中描述Interval和Timerout的时间计数是不精准的原因。

最后,借StackExchange中的@hyde的一段描述来补充以及结束这篇跑题的知乎回答。

Threads are often used, when there is some processing which takes long time. If it were done in the main thread with event loop, the long operation would have to be chopped into small pieces, so other events could be handled too. So it's simpler to have a worker thread, which can do the processing without interfering with event processing, and then generate event for main event loop when done.

在通用的设计结构中,event loop和call stack是可以混用一个线程的,比如Tornado。但倘若某些代码会花费大量时间来执行,通常Guide中则会不得不建议将这段代码拆分成多个分段来保证轮询调度的效率(毕竟不是抢占式调度),所以如果event loop有独立的线程则会使得代码运转的更自然,因此对应在浏览器中,UI Rendering与JS Call Stack共用了线程(如果我没记错的话),轮询机制由浏览器内建;而Node.js中,轮询则由libuv提供的,并且libuv建立了针对不同kernel的抽象,封装了更多IO有关的具体的处理场景以及woker线程,这也解释了为什么Node.js单节点拥有高负载的原因。

而我们在现实场景中接触到的JavaScript有关的软件架构可能会更接近抽象于图一的这张图。


Ref:
Events, Concurrency and JavaScript
Event Driven Programming?
John Resig - How JavaScript Timers Work