javascript运行机制详解: 再出口Event Loop(转)

作者: 阮一峰

日期: 2014年10月 8日

相同年前,我写了千篇一律首《什么是 Event
Loop?》,谈了自我本着Event
Loop的领悟。

上个月,我偶然见到了Philip Roberts的讲演《Help, I’m stuck in an
event-loop》。这才尴尬地意识,自己的喻是错的。我说了算还写是问题,详细、完整、正确地叙述JavaScript引擎的里运行机制。下面就是本身之重写。

入正文之前,插播一长达信息。我的新书《ECMAScript
6入门》出版了(版权页,内页1,内页2),铜版纸全彩印刷,非常好,还从索引(当然价格也较同类书籍略贵一点点)。预览和购进点击这里。

Node.js 1

(2014年10月13日翻新:本文就召开了比较生改,反映了自家现的认识。关于setTimeout的双重多讲以及演示,请参见我在写的《JavaScript标准参照教程》。)

(2014年10月11日翻新:朴灵先生对本文做了评注,详细得指出了文中有的缪说法,建议看。)

同一、为什么JavaScript是单线程?

JavaScript语言的一样万分特色就是单线程,也就是说,同一个时光只能开同样件事。那么,为什么JavaScript不克产生多单线程呢?这样会提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它们只能是单线程,否则会带好复杂的旅问题。比如,假定JavaScript同时发生少独线程,一个线程在有DOM节点上加加内容,另一个线程删除了是节点,这时浏览器应该坐谁线程为按照?

为此,为了避免复杂性,从同生,JavaScript就是单线程,这早就化为了当下宗语言的主干特征,将来啊未会见改变。

以以基本上核CPU的精打细算能力,HTML5提出Web
Worker标准,允许JavaScript脚论创建多单线程,但是子线程完全被主线程控制,且不得操作DOM。所以,这个新规范并无更改JavaScript单线程的本色。

老二、任务队列

单线程就象征,所有任务急需排队,前一个任务了,才会实施后一个职责。如果前方一个职责耗时很丰富,后一个任务便只好直接当在。

假设排队是坐计算量大,CPU忙不过来,倒也算是了,但是不少辰光CPU是空在的,因为IO设备(输入输出设备)很缓慢(比如Ajax操作由网络读取数据),不得不等正结果出来,再往下实施。

JavaScript语言的设计者意识及,这时主线程完全可以不管IO设备,挂于处于等候中之天职,先运行排在后边的任务。等交IO设备返回了结果,再转了头,把挂于底任务继续执行下去。

乃,所有任务可以分成两种,一栽是同步任务(synchronous),另一样种植是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务履行了,才能够尽后一个职责;异步任务指的是,不进来主线程、而进”任务队列”(task
queue)的天职,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进去主线程执行。

具体来说,异步执行的运行机制如下。(同步执行为是这般,因为它们好让视为没有异步任务的异步执行。)

(1)所有联合任务还当主线程上实施,形成一个执行栈(execution
context stack)。

(2)主线程之外,还留存一个”任务队列”(task
queue)。只要异步任务产生矣运行结果,就以”任务队列”之中放置一个事变。

(3)一旦”执行栈”中之备联合任务尽完毕,系统即见面读取”任务队列”,看看里面有哪事件。那些对应之异步任务,于是结束等待状态,进入执行栈,开始实施。

(4)主线程不断重复上面的老三步。

产图虽是主线程和职责队列的示意图。

Node.js 2

倘主线程空了,就见面去读取”任务队列”,这就是是JavaScript的运行机制。这个进程会随地重复。

老三、事件与回调函数

“任务队列”是一个事变之阵(也得以解成信息的队列),IO设备就同样宗任务,就以”任务队列”中补充加一个事件,表示有关的异步任务可入”执行栈”了。主线程读取”任务队列”,就是读取里面来安事件。

“任务队列”中之轩然大波,除了IO设备的轩然大波外,还包部分用户发生的风波(比如鼠标点击、页面滚动等等)。只要指定了回调函数,这些事件有常就见面进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会受主线程挂起来的代码。异步任务要指定回调函数,当主线程开执行异步任务,就是实施相应之回调函数。

“任务队列”是一个先进先出的数据结构,排在前的事件,优先让主线程读取。主线程的读取过程基本上是电动的,只要实行栈一清空,”任务队列”上先是各项的事件便自动进入主线程。但是,由于在后文提到的”定时器”功能,主线程首先要检查一下执行时,某些事件只是生到了规定的时,才能够回来主线程。

四、Event Loop

主线程从”任务队列”中读取事件,这个进程是时时刻刻的,所以总体的这种运行机制又称为Event
Loop(事件循环)。

为还好地理解Event Loop,请圈下图(转引自Philip Roberts的演讲《Help,
I’m stuck in an event-loop》)。

Node.js 3

及图被,主线程运行的当儿,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们当”任务队列”中投入各种风波(click,load,done)。只要栈中的代码执行完毕,主线程就会失掉读取”任务队列”,依次执行那些事件所对应之回调函数。

履行栈中的代码(同步任务),总是以读取”任务队列”(异步任务)之前实施。请圈下面是例子。

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

方代码中的req.send方法是Ajax操作为服务器发送数据,它是一个异步任务,意味着只有当前剧本的享有代码执行完,系统才会失掉读取”任务队列”。所以,它与下部的写法等价格。

    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定回调函数的片段(onload和onerror),在send()方法的先头或后无关紧要,因为它属于执行栈的均等片,系统总是执行完毕它们,才见面去读取”任务队列”。

五、定时器

除去放置异步任务之风波,”任务队列”还可停定时事件,即指定某些代码在多少日子以后执行。这名”定时器”(timer)功能,也就算是定时执行之代码。

定时器功能重要由于setTimeout()和setInterval()这片只函数来就,它们的内运行机制完全一样,区别在前者指定的代码是一次性执行,后者则为数实践。以下重点讨论setTimeout()。

setTimeout()接受两个参数,第一个凡是回调函数,第二单凡是推执行之毫秒数。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的推行结果是1,3,2,因为setTimeout()将第二履行推迟至1000毫秒之后执行。

万一将setTimeout()的老二个参数设为0,就表示即代码执行完(执行栈清空)以后,立即实施(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的实行结果总是2,1,因为只有在实践了第二履行以后,系统才见面错过实践”任务队列”中之回调函数。

一言以蔽之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得之空余时实施,也就是说,尽可能早得执行。它以”任务队列”的尾巴添加一个风波,因此要等到同步任务与”任务队列”现有的波还处理完,才见面拿走实施。

HTML5标准规定了setTimeout()的第二单参数的顶小值(最差间隔),不得低于4毫秒,如果低于这价值,就会自动增加。在此之前,老版本的浏览器都将最为缺少间隔设为10毫秒。另外,对于那些DOM的改动(尤其是关乎页面还渲染的一部分),通常不见面立即执行,而是每16毫秒执行同样次等。这时用requestAnimationFrame()的效果使好于setTimeout()。

欲注意的是,setTimeout()只是拿事件插入了”任务队列”,必须等交当下代码(执行栈)执行完毕,主线程才见面失去履行其指定的回调函数。要是当前代码耗时很丰富,有或要当老长远,所以并无办法保证,回调函数一定会以setTimeout()指定的光阴实施。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同为浏览器环境。

要圈下面的示意图(作者@BusyRich)。

Node.js 4

据悉上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库荷Node
API的行。它以不同的任务分配给不同之线程,形成一个Event
Loop(事件循环),以异步的不二法门将任务的实施结果回到给V8引擎。

(4)V8引擎再用结果回到给用户。

除了setTimeout和setInterval这简单只主意,Node.jsNode.js还提供了另外两单同”任务队列”有关的艺术:process.nextTick和setImmediate。它们可拉我们深化对”任务队列”的懂得。

process.nextTick方法可以于现阶段”执行栈”的尾部—-下一致次等Event
Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的天职连续发出在具有异步任务之前。setImmediate方法虽然是以现阶段”任务队列”的尾部添加事件,也就是说,它指定的职责连续以产同样糟Event
Loop时实行,这与setTimeout(fn,
0)很像。请圈下面的事例(via StackOverflow)。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

方代码中,由于process.nextTick方法指定的回调函数,总是以现阶段”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果发生多单process.nextTick语句(不管它是否嵌套),将所有以脚下”执行栈”执行。

现在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

方代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是以产同样糟Event
Loop触发。那么,哪个回调函数先实施呢?答案是未确定。运行结果也许是1–TIMEOUT
FIRED–2,也说不定是TIMEOUT FIRED–1–2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种景象无非来在递归调用的下。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

地方代码中,setImmediate和setTimeout被封闭装在一个setImmediate里面,它的运行结果连续1–TIMEOUT
FIRED–2,这时函数A一定在timeout前面触发。至于2免除在TIMEOUT
FIRED的末端(即函数B在timeout后面触发),是以setImmediate总是用事件注册到下一样轮Event
Loop,所以函数A和timeout是以同一轱辘Loop执行,而函数B在生同样车轮Loop执行。

咱俩通过得到了process.nextTick和setImmediate的一个主要区别:多独process.nextTick语词总是以此时此刻”执行栈”一蹩脚实施完毕,多单setImmediate可能则需频繁loop才会实施了。事实上,这多亏Node.js
10.0本上加setImmediate方法的原委,否则像下这样的递归调用process.nextTick,将见面没完没了,主线程根本不会见失掉读取”事件队列”!

process.nextTick(function foo() {
  process.nextTick(foo);
});

实际,现在若是你写有递归的process.nextTick,Node.js会抛来一个警戒,要求而转移成为setImmediate。

除此以外,由于process.nextTick指定的回调函数是于此次”事件循环”触发,而setImmediate指定的是以下次”事件循环”触发,所以十分鲜明,前者总是比继承者来得早,而且实施效率也高(因为不用检查”任务队列”)。

(完)