带你完全弄懂Event Loop

媒介

我在进修浏览器和NodeJS的Event Loop时看了大批的文章,那些文章都写的很好,然则往往是每篇文章有那末几个症结的点,许多篇文章凑在一同综合来看,才够对这些观点有较为深切的邃晓。

因而,我在看了大批文章以后,想要写这么一篇博客,不采纳官方的形貌,连系本身的邃晓以及示例代码,用最浅显的言语表达出来。愿望人人能够经由历程这篇文章,相识到Event Loop究竟是一种什么机制,浏览器和NodeJS的Event Loop又有什么区分。假如在文中涌现誊写毛病的处所,迎接人人留言一同议论。

(PS:说到Event Loop一定会提到Promise,我依据Promise A+范例本身完成了一个浅易Promise库,源码放到Github上,人人有须要的能够当作参考,后续我也会也写一篇博客来讲Promise,假如对你有效,就请给个Star吧~)

正文

Event Loop是什么

event loop是一个实行模子,在差别的处一切差别的完成。浏览器和NodeJS基于差别的手艺完成了各自的Event Loop。

  • 浏览器的Event Loop是在html5的范例中邃晓定义。
  • NodeJS的Event Loop是基于libuv完成的。能够参考Node的官方文档以及libuv的官方文档
  • libuv已对Event Loop做出了完成,而HTML5范例中只是定义了浏览器中Event Loop的模子,细致的完成留给了浏览器厂商。

宏行列和微行列

宏行列,macrotask,也叫tasks。 一些异步使命的回调会顺次进入macro task queue,守候后续被挪用,这些异步使命包含:

  • setTimeout
  • setInterval
  • setImmediate (Node独占)
  • requestAnimationFrame (浏览器独占)
  • I/O
  • UI rendering (浏览器独占)

微行列,microtask,也叫jobs。 另一些异步使命的回调会顺次进入micro task queue,守候后续被挪用,这些异步使命包含:

  • process.nextTick (Node独占)
  • Promise
  • Object.observe
  • MutationObserver

(注:这里只针对浏览器和NodeJS)

浏览器的Event Loop

我们先来看一张图,再看完这篇文章后,请返返来再细致看一下这张图,置信你会有更深的邃晓。

《带你完全弄懂Event Loop》

这张图将浏览器的Event Loop完全的形貌了出来,我来讲实行一个JavaScript代码的细致流程:

  1. 实行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比方setTimeout等);
  2. 全局Script代码实行终了后,挪用栈Stack会清空;
  3. 从微行列microtask queue中掏出位于队首的回调使命,放入挪用栈Stack中实行,实行完后microtask queue长度减1;
  4. 继承掏出位于队首的使命,放入挪用栈Stack中实行,以此类推,直到直到把microtask queue中的一切使命都实行终了。注重,假如在实行microtask的历程当中,又发生了microtask,那末会加入到行列的末端,也会在这个周期被挪用实行
  5. microtask queue中的一切使命都实行终了,此时microtask queue为空行列,挪用栈Stack也为空;
  6. 掏出宏行列macrotask queue中位于队首的使命,放入Stack中实行;
  7. 实行终了后,挪用栈Stack为空;
  8. 反复第3-7个步骤;
  9. 反复第3-7个步骤;
  10. ……

能够看到,这就是浏览器的事宜轮回Event Loop

这里归结3个重点:

  1. 宏行列macrotask一次只从行列中取一个使命实行,实行完后就去实行微使命行列中的使命;
  2. 微使命行列中一切的使命都邑被顺次掏出来实行,晓得microtask queue为空;
  3. 图中没有画UI rendering的节点,因为这个是由浏览器自行推断决定的,然则只需实行UI rendering,它的节点是在实行完一切的microtask以后,下一个macrotask之前,紧跟着实行UI render。

好了,观点性的东西就这么多,来看几个示例代码,测试一下你是不是控制了:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);

这里结果会是什么呢?应用上面相识到的学问,先本身做一下碰运气。

// 准确答案
1
4
7
5
2
3
6

你答对了吗?

我们来剖析一下悉数流程:

  • 实行全局Script代码

Step 1

console.log(1)

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印结果:

1

Step 2

setTimeout(() => {
  // 这个回调函数叫做callback1,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

打印结果:

1

Step 3

new Promise((resolve, reject) => {
  // 注重,这里是同步实行的,假如不太清晰,能够去看一下我开首本身完成的promise啦~~
  console.log(4)
  resolve(5)
}).then((data) => {
  // 这个回调函数叫做callback2,promise属于microtask,所以放到microtask queue中
  console.log(data);
})

Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

打印结果:

1

4

Step 5

setTimeout(() => {
  // 这个回调函数叫做callback3,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(6);
})

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:

1

4

Step 6

console.log(7)

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:

1

4

7

  • 好啦,全局Script代码实行完了,进入下一个步骤,从microtask queue中顺次掏出使命实行,直到microtask queue行列为空。

Step 7

console.log(data)       // 这里data是Promise的决定值5

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

打印结果:

1

4

7

5

  • 这里microtask queue中只要一个使命,实行完后最先从宏使命行列macrotask queue中取位于队首的使命实行

Step 8

console.log(2)

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:

1

4

7

5

2

然则,实行callback1的时刻又遇到了另一个Promise,Promise异步实行完后在microtask queue中又注册了一个callback4回调函数

Step 9

Promise.resolve().then(() => {
  // 这个回调函数叫做callback4,promise属于microtask,所以放到microtask queue中
  console.log(3)
});

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

打印结果:

1

4

7

5

2

  • 掏出一个宏使命macrotask实行终了,然后再去微使命行列microtask queue中顺次掏出实行

Step 10

console.log(3)

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:

1

4

7

5

2

3

  • 微使命行列悉数实行完,再去宏使命行列中取第一个使命实行

Step 11

console.log(6)

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

打印结果:

1

4

7

5

2

3

6

  • 以上,悉数实行完后,Stack Queue为空,Macrotask Queue为空,Micro Queue为空

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

终究打印结果:

1

4

7

5

2

3

6

因为是第一个例子,所以这里剖析的比较细致,人人细致看一下,接下来我们再来一个例子:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

终究输出结果是什么呢?参考前面的例子,好好想想……

// 准确答案
1
4
10
5
6
7
2
3
9
8

置信人人都答对了,这里的症结在前面已提过:

在实行微行列microtask queue中使命的时刻,假如又发生了microtask,那末会继承添加到行列的末端,也会在这个周期实行,直到microtask queue为空住手。

注:固然假如你在microtask中不停的发生microtask,那末其他宏使命macrotask就没法实行了,然则这个操纵也不是无穷的,拿NodeJS中的微使命process.nextTick()来讲,它的上限是1000个,背面我们会讲到。

浏览器的Event Loop就说到这里,下面我们看一下NodeJS中的Event Loop,它更庞杂一些,机制也不太一样。

NodeJS中的Event Loop

libuv

先来看一张libuv的结构图:

《带你完全弄懂Event Loop》

NodeJS中的宏行列和微行列

NodeJS的Event Loop中,实行宏行列的回调使命有6个阶段,以下图:

《带你完全弄懂Event Loop》

各个阶段实行的使命以下:

  • timers阶段:这个阶段实行setTimeout和setInterval预定的callback
  • I/O callback阶段:实行除了close事宜的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些以外的callbacks
  • idle, prepare阶段:仅node内部运用
  • poll阶段:猎取新的I/O事宜,恰当的条件下node将壅塞在这里
  • check阶段:实行setImmediate()设定的callbacks
  • close callbacks阶段:实行socket.on(‘close’, ….)这些callbacks

NodeJS中宏行列重要有4个

由上面的引见能够看到,回调事宜重要位于4个macrotask queue中:

  1. Timers Queue
  2. IO Callbacks Queue
  3. Check Queue
  4. Close Callbacks Queue

这4个都属于宏行列,然则在浏览器中,能够以为只要一个宏行列,一切的macrotask都邑被加到这一个宏行列中,然则在NodeJS中,差别的macrotask会被安排在差别的宏行列中。

NodeJS中微行列重要有2个

  1. Next Tick Queue:是安排process.nextTick(callback)的回调使命的
  2. Other Micro Queue:安排其他microtask,比方Promise等

在浏览器中,也能够以为只要一个微行列,一切的microtask都邑被加到这一个微行列中,然则在NodeJS中,差别的microtask会被安排在差别的微行列中。

细致能够经由历程下图加深一下邃晓:

《带你完全弄懂Event Loop》

大致诠释一下NodeJS的Event Loop历程:

  1. 实行全局Script的同步代码
  2. 实行microtask微使命,先实行一切Next Tick Queue中的一切使命,再实行Other Microtask Queue中的一切使命
  3. 最先实行macrotask宏使命,共6个阶段,从第1个阶段最先实行响应每一个阶段macrotask中的一切使命,注重,这里是一切每一个阶段宏使命行列的一切使命,在浏览器的Event Loop中是只取宏行列的第一个使命出来实行,每一个阶段的macrotask使命实行终了后,最先实行微使命,也就是步骤2
  4. Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ……
  5. 这就是Node的Event Loop

关于NodeJS的macrotask queue和microtask queue,我画了两张图,人人作为参考:

《带你完全弄懂Event Loop》

《带你完全弄懂Event Loop》

好啦,观点邃晓了我们经由历程几个例子来实战一下:

第一个例子

console.log('start');

setTimeout(() => {          // callback1
  console.log(111);
  setTimeout(() => {        // callback2
    console.log(222);
  }, 0);
  setImmediate(() => {      // callback3
    console.log(333);
  })
  process.nextTick(() => {  // callback4
    console.log(444);  
  })
}, 0);

setImmediate(() => {        // callback5
  console.log(555);
  process.nextTick(() => {  // callback6
    console.log(666);  
  })
})

setTimeout(() => {          // callback7              
  console.log(777);
  process.nextTick(() => {  // callback8
    console.log(888);   
  })
}, 0);

process.nextTick(() => {    // callback9
  console.log(999);  
})

console.log('end');

请应用前面学到的学问,细致剖析一下……

// 准确答案
start
end
999
111
777
444
888
555
333
666
222

更新 2018.9.20

上面这段代码你实行的结果能够会有多种状况,缘由诠释以下。

  • setTimeout(fn, 0)不是严厉的0,平常是setTimeout(fn, 3)或什么,会有一定的延迟时候,当setTimeout(fn, 0)和setImmediate(fn)涌现在统一段同步代码中时,就会存在两种状况。
  • 第1种状况:同步代码实行完了,Timer还没到期,setImmediate回调先注册到Check Queue中,最先实行微行列,然后是宏行列,先从Timers Queue中最先,发明没回调,往下走直到Check Queue中有回调,实行,然后timer到期(只需在实行完Timer Queue后到期结果就都一样),timer回调注册到Timers Queue中,下一轮轮回实行到Timers Queue中才实行谁人timer 回调;所以,这类状况下,setImmediate(fn)回调先于setTimeout(fn, 0)回调实行
  • 第2种状况:同步代码还没实行完,timer先到期,timer回调先注册到Timers Queue中,实行到setImmediate了,它的回调再注册到Check Queue中。 然后,同步代码实行完了,实行微行列,然后最先先实行Timers Queue,先实行Timer 回调,再到Check Queue,实行setImmediate回调;所以,这类状况下,setTimeout(fn, 0)回调先于setImmediate(fn)回调实行
  • 所以,在同步代码中同时调setTimeout(fn, 0)和setImmediate状况是不肯定的,然则假如把他们放在一个IO的回调,比方readFile(‘xx’, function () {// ….})回调中,那末IO回调是在IO Queue中,setTimeout到期回调注册到Timers Queue,setImmediate回调注册到Check Queue,IO Queue实行完到Check Queue,timer Queue获得下个周期,所以setImmediate回调这类状况下一定比setTimeout(fn, 0)回调先实行。

综上,这个例子是不太好的,setTimeout(fn, 0)和setImmediate(fn)假如想要保证结果唯一,就放在一个IO Callback中吧,上面那段代码能够把一切它俩同步实行的代码都放在一个IO Callback中,结果就唯一了。

更新完毕

你答对了吗?我们来一同剖析一下:

  • 实行全局Script代码,先打印start,向下实行,将setTimeout的回调callback1注册到Timers Queue中,再向下实行,将setImmediate的回调callback5注册到Check Queue中,接着向下实行,将setTimeout的回调callback7注册到Timers Queue中,继承向下,将process.nextTick的回调callback9注册到微行列Next Tick Queue中,末了一步打印end。此时,各个行列的回调状况以下:

宏行列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: [callback9]

Other Microtask Queue: []

打印结果

start

end

  • 全局Script实行完了,最先顺次实行微使命Next Tick Queue中的悉数回调使命。此时Next Tick Queue中只要一个callback9,将其掏出放入挪用栈中实行,打印999

宏行列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: []

Other Microtask Queue: []

打印结果

start

end

999

  • 最先顺次实行6个阶段各自宏行列中的一切使命,先实行第1个阶段Timers Queue中的一切使命,先掏出callback1实行,打印111,callback1函数继承向下,顺次把callback2放入Timers Queue中,把callback3放入Check Queue中,把callback4放入Next Tick Queue中,然后callback1实行终了。再掏出Timers Queue中此时排在首位的callback7实行,打印777,把callback8放入Next Tick Queue中,实行终了。此时,各行列状况以下:

宏行列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: [callback4, callback8]

Other Microtask Queue: []

打印结果

start

end

999

111

777

  • 6个阶段每阶段的宏使命行列实行终了后,都邑最先实行微使命,此时,先掏出Next Tick Queue中的一切使命实行,callback4最先实行,打印444,然后callback8最先实行,打印888,Next Tick Queue实行终了,最先实行Other Microtask Queue中的使命,因为内里为空,所以继承向下。

宏行列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: []

Other Microtask Queue: []

打印结果

start

end

999

111

777

444

888

  • 第2个阶段IO Callback Queue行列为空,跳过,第3和第4个阶段平常是Node内部运用,跳过,进入第5个阶段Check Queue。掏出callback5实行,打印555,把callback6放入Next Tick Queue中,实行callback3,打印333

宏行列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印结果

start

end

999

111

777

444

888

555

333

  • 实行微使命行列,先实行Next Tick Queue,掏出callback6实行,打印666,实行终了,因为Other Microtask Queue为空,跳过。

宏行列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印结果

start

end

999

111

777

444

888

555

333

  • 实行第6个阶段Close Callback Queue中的使命,为空,跳过,好了,此时一个轮回已完毕。进入下一个轮回,实行第1个阶段Timers Queue中的一切使命,掏出callback2实行,打印222,终了。此时,一切行列包含宏使命行列和微使命行列都为空,不再打印任何东西。

宏行列

Timers Queue: []

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微行列

Next Tick Queue: [callback6]

Other Microtask Queue: []

终究结果

start

end

999

111

777

444

888

555

333

666

222

以上就是这道问题的细致剖析,假如没有邃晓,一定要多看频频。

下面引入Promise再来看一个例子:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
process.nextTick(function() {
  console.log('6');
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

人人细致剖析,比拟于上一个例子,这里因为存在Promise,所以Other Microtask Queue中也会有回调使命的存在,实行到微使命阶段时,先实行Next Tick Queue中的一切使命,再实行Other Microtask Queue中的一切使命,然后才会进入下一个阶段的宏使命。邃晓了这一点,置信人人都能够剖析出来,下面直接给出准确答案,若有疑问,迎接留言和我议论。

// 准确答案
1
7
6
8
2
4
9
11
3
10
5
12

setTimeout 对照 setImmediate

  • setTimeout(fn, 0)在Timers阶段实行,并且是在poll阶段举行推断是不是到达指定的timer时候才会实行
  • setImmediate(fn)在Check阶段实行

二者的实行递次要依据当前的实行环境才肯定:

  • 假如二者都在主模块(main module)挪用,那末实行前后取决于历程机能,递次随机
  • 假如二者都不在主模块挪用,即在一个I/O Circle中挪用,那末setImmediate的回调永久先实行,因为会先到Check阶段

setImmediate 对照 process.nextTick

  • setImmediate(fn)的回调使命会插进去到宏行列Check Queue中
  • process.nextTick(fn)的回调使命会插进去到微行列Next Tick Queue中
  • process.nextTick(fn)挪用深度有限定,上限是1000,而setImmedaite则没有

总结

  1. 浏览器的Event Loop和NodeJS的Event Loop是差别的,完成机制也不一样,不要等量齐观。
  2. 浏览器能够邃晓成只要1个宏使命行列和1个微使命行列,先实行全局Script代码,实行完同步代码挪用栈清空后,从微使命行列中顺次掏出一切的使命放入挪用栈实行,微使命行列清空后,从宏使命行列中只取位于队首的使命放入挪用栈实行,注重这里和Node的区分,只取一个,然后继承实行微行列中的一切使命,再去宏行列取一个,以此组成事宜轮回。
  3. NodeJS能够邃晓成有4个宏使命行列和2个微使命行列,然则实行宏使命时有6个阶段。先实行全局Script代码,实行完同步代码挪用栈清空后,先从微使命行列Next Tick Queue中顺次掏出一切的使命放入挪用栈中实行,再从微使命行列Other Microtask Queue中顺次掏出一切的使命放入挪用栈中实行。然后最先宏使命的6个阶段,每一个阶段都将该宏使命行列中的一切使命都掏出来实行(注重,这里和浏览器不一样,浏览器只取一个),每一个宏使命阶段实行终了后,最先实行微使命,再最先实行下一阶段宏使命,以此组成事宜轮回。
  4. MacroTask包含: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(浏览器)、IO、UI rendering
  5. Microtask包含: process.nextTick(Node)、Promise、Object.observe、MutationObserver

迎接关注我的民众号

《带你完全弄懂Event Loop》

参考链接

不要殽杂nodejs和浏览器中的event loop
node中的Event模块
Promises, process.nextTick And setImmediate
浏览器和Node差别的事宜轮回
Tasks, microtasks, queues and schedules
邃晓事宜轮回浅析

    原文作者:liuxuan
    原文地址: https://segmentfault.com/a/1190000016278115
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞