Loading…

以下内容整理于关于浏览器处理事件的问题?,配合博客做了一定修改,题目看起来很大,但其实没几句话。(有朋友关心的:ng2,我抽空会再研究下,然后post出来的)

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

但凡这种「既是单线程又是异步」的语言有一个共同特点:它们是 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暴露的、提供的异步入口。而另一方面,像ECMAScript6 提供的Generator的特性,就其本身来说只是个迭代器,在诸如Koa等应用中,它的异步能力依然是由V8所提供的。

(上图描述了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在第二次触发时,上一次的回调仍未获得执行,则该次调用自动被抛弃。 上述情形伪码类似下面的代码,假设默认存在Timer Queue对象,则Interval事件如下表现:

    var expr= (e) => e.type=='Interval' && e.status=='pending'
    if(TimerQueue.filter(expr).length>=1)
        IntervalEvent.dropped
    else
        IntervalEvent.fired

这也是为什么,红宝书中描述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,但倘若某些代码会花费大量时间来执行,那我们则不得不建议将这段代码拆分成多个分段来保证轮询调度的效率(毕竟不是抢占式调度),所以如果event loop有独立的线程则会使得代码运转的更自然,因此对应在浏览器中,UI Rendering与JS call stack共用了线程(如果没记错的话),轮询机制由浏览器内建;而Node.js中,轮询则是libuv提供的,并且由libuv建立了针对不同kernel的抽象,封装了多个用于处理IO的线程,这也解释了为什么Node.js单节点拥有高负载的原因。

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

P.S:这篇文章也算是一定程度上还了BigPipe实现的那篇的债,写了一部分当时在那篇文章中构想的内容。

Ref:

About the author
comments powered by Disqus