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),铜版纸全彩印刷,非常优秀,还附有索引(当然价格为比同类书籍略贵一点点)。预览和打点击这里。

ECMAScript 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)主线程不断重复上面的老三步。

下图虽是主线程和天职队列的示意图。

ECMAScript 2

苟主线程空了,就见面去读取”任务队列”,这便是JavaScript的运行机制。这个历程会频频重复。

老三、事件与回调函数

“任务队列”是一个波的队(也可解成信息之阵),IO设备就同样件职责,就以”任务队列”中上加一个风波,表示相关的异步任务可进去”执行栈”了。主线程读取”任务队列”,就是读取里面来怎样事件。

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

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

“任务队列”是一个先进先出的数据结构,排在面前的轩然大波,优先让主线程读取。主线程的读取过程基本上是机动的,只要实行栈一清空,”任务队列”上先是位之轩然大波便自行进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先使检查一下执行时间,某些事件只是来到了确定之工夫,才会回主线程。

四、Event Loop

主线程从”任务队列”中读取事件,这个进程是络绎不绝的,所以整个的这种运行机制又称作Event
Loop(事件循环)。

以还好地领略Event Loop,请看下图(转引自Philip Roberts的发言《Help,
I’m stuck in an event-loop》)。

ECMAScript 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)。

ECMAScript 4

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

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

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

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

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

除此之外setTimeout和setInterval这半独方法,Node.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指定的凡当下次”事件循环”触发,所以十分显著,前者总是比后者来得早,而且实施效率也愈(因为不用检查”任务队列”)。

(完)