浏览器事件循环

image.png

浏览器页面是通过事件循环机制来驱动的,每个渲染进程都有一个消息队列,页面主线程按照顺序来执行消息队列中的事件,如执行 JavaScript 事件、解析 DOM 事件、计算布局事件、用户输入事件等等,如果页面有新的事件产生,那新的事件将会追加到事件队列的尾部。所以可以说是消息队列和主线程循环机制保证了页面有条不紊地运行。

从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程

  • 添加一个消息队列;
  • IO 线程中产生的新任务添加进消息队列尾部;
  • 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

浏览器是单线程的,弊端:

  1. 单个任务执行过久,导致页面卡顿: 回调函数callabck/promise/async
  2. 优先级分配:消息队列分类-微任务和宏任务

通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。
等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

宏任务:setTimeout/XMLHttpRequest
微任务:Promise(resolve、reject)/MutationObserver

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: process.nextTick、new Promise().then(回调)、MutationObserver(html5新特性) 等。

如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
image.png

运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
  1. Promise.resolve().then(()=>{
  2. console.log('Promise1')
  3. setTimeout(()=>{
  4. console.log('setTimeout2')
  5. },0)
  6. })
  7. setTimeout(()=>{
  8. console.log('setTimeout1')
  9. Promise.resolve().then(()=>{
  10. console.log('Promise2')
  11. })
  12. },0)

image.png

  1. async function async1() {
  2. console.log('async1 start');
  3. await async2();
  4. console.log('async1 end');
  5. }
  6. 等价于
  7. async function async1() {
  8. console.log('async1 start');
  9. Promise.resolve(async2()).then(() => {
  10. console.log('async1 end');
  11. })
  12. }
  1. console.log('A');
  2. // prmomise链式
  3. var promise = new Promise((resolve, reject) => {
  4. console.log('C');
  5. // 10ms之后resolve 才执行then
  6. setTimeout(() => {
  7. console.log('D');
  8. resolve();
  9. reject();
  10. resolve();
  11. }, 10);
  12. // 先到期先执行
  13. setTimeout(() => {
  14. console.log('H');
  15. });
  16. });
  17. promise.then((res) => {
  18. console.log('E');
  19. });
  20. promise.then((res) => {
  21. console.log('F');
  22. });
  23. promise.catch((res) => {
  24. console.log('G'); //不会打印,状态先resolve
  25. });
  26. console.log('B');
  27. // ACBHDEF
  1. //请写出输出内容
  2. async function async1() {
  3. console.log('async1 start');
  4. await async2();
  5. console.log('async1 end');
  6. }
  7. async function async2() {
  8. console.log('async2');
  9. }
  10. console.log('script start');
  11. setTimeout(function() {
  12. console.log('setTimeout');
  13. }, 0)
  14. async1();
  15. new Promise(function(resolve) {
  16. console.log('promise1');
  17. resolve();
  18. }).then(function() {
  19. console.log('promise2');
  20. });
  21. console.log('script end');
  22. /*
  23. script start
  24. async1 start
  25. async2
  26. promise1
  27. script end
  28. async1 end
  29. promise2
  30. setTimeout
  31. */
  1. async function async1() {
  2. console.log('async1 start');
  3. await async2();
  4. console.log('async1 end');
  5. }
  6. async function async2() {
  7. //async2做出如下更改:
  8. new Promise(function(resolve) {
  9. console.log('promise1');
  10. resolve();
  11. }).then(function() {
  12. console.log('promise2');
  13. });
  14. }
  15. console.log('script start');
  16. setTimeout(function() {
  17. console.log('setTimeout');
  18. }, 0)
  19. async1();
  20. new Promise(function(resolve) {
  21. console.log('promise3');
  22. resolve();
  23. }).then(function() {
  24. console.log('promise4');
  25. });
  26. console.log('script end');
  27. script start
  28. async1 start
  29. promise1
  30. promise3
  31. script end
  32. promise2
  33. async1 end
  34. promise4
  35. setTimeout
  1. async function async1() {
  2. console.log('async1 start');
  3. await async2();
  4. //更改如下:
  5. setTimeout(function() {
  6. console.log('setTimeout1')
  7. },0)
  8. }
  9. async function async2() {
  10. //更改如下:
  11. setTimeout(function() {
  12. console.log('setTimeout2')
  13. },0)
  14. }
  15. console.log('script start');
  16. setTimeout(function() {
  17. console.log('setTimeout3');
  18. }, 0)
  19. async1();
  20. new Promise(function(resolve) {
  21. console.log('promise1');
  22. resolve();
  23. }).then(function() {
  24. console.log('promise2');
  25. });
  26. console.log('script end');
  27. script start
  28. async1 start
  29. promise1
  30. script end
  31. promise2
  32. setTimeout3
  33. setTimeout2
  34. setTimeout1
  1. async function a1 () {
  2. console.log('a1 start')
  3. await a2()
  4. console.log('a1 end')
  5. }
  6. async function a2 () {
  7. console.log('a2')
  8. }
  9. console.log('script start')
  10. setTimeout(() => {
  11. console.log('setTimeout')
  12. }, 0)
  13. Promise.resolve().then(() => {
  14. console.log('promise1')
  15. })
  16. a1()
  17. let promise2 = new Promise((resolve) => {
  18. resolve('promise2.then')
  19. console.log('promise2')
  20. })
  21. promise2.then((res) => {
  22. console.log(res)
  23. Promise.resolve().then(() => {
  24. console.log('promise3')
  25. })
  26. })
  27. console.log('script end')
  28. script start
  29. a1 start
  30. a2
  31. promise2
  32. script end
  33. promise1
  34. a1 end
  35. promise2.then
  36. promise3
  37. setTimeout

Node事件循环

image.png

先解释一下各个阶段

  1. timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
  2. I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
  3. idle, prepare: 仅内部使用。
  4. poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。
  5. check: 执行setImmediate()设定的回调。
  6. close callbacks: 执行比如socket.on(‘close’, …)的回调。

每个阶段的详情

timer
  • timers 阶段会执行 setTimeoutsetInterval
  • 一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟

I/O

  • I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

poll

poll 阶段的功能有两个

  • 执行 timer 阶段到达时间上限的任务。
  • 执行 poll 阶段的任务队列。

如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况

  • 如果 poll 队列不为空的话,会执行 poll 队列直到清空或者系统回调数达到上限
  • 如果 poll 队列为空
    如果设定了 setImmediate 回调,会直接跳到 check 阶段。 如果没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并立即执行。

    check
  • check 阶段执行 setImmediate

这个阶段在 poll 结束后立即执行,setImmediate 的回调会在这里执行。
一般来说,event loop 肯定会进入 poll 阶段,当没有 poll 任务时,会等待新的任务出现,但如果设定了 setImmediate,会直接执行进入下个阶段而不是继续等。

close

close 事件在这里触发,否则将通过 process.nextTick 触发。

特殊

setTimeout与setImmediate
由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。

  1. setTimeout(() => {
  2. console.log('setTimeout');
  3. }, 0);
  4. setImmediate(() => {
  5. console.log('setImmediate');
  6. })
  7. // 这里可能会输出 setTimeout,setImmediate
  8. // 可能也会相反的输出,这取决于性能
  9. // 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
  10. // 否则会执行 setTimeout

setTimeout的第二个参数默认为0。实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)。
实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

如果在IO操作中回调中,setImmediate先于setTimeout
下面的代码一定是先输出2,再输出1。

  1. const fs = require('fs');
  2. fs.readFile('test.js', () => {
  3. setTimeout(() => console.log(1));
  4. setImmediate(() => console.log(2));
  5. });
  6. 复制代码

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

process.nextTick

独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

  1. setTimeout(() => {
  2. console.log('timer1')
  3. Promise.resolve().then(function() {
  4. console.log('promise1')
  5. })
  6. }, 0)
  7. process.nextTick(() => {
  8. console.log('nextTick')
  9. process.nextTick(() => {
  10. console.log('nextTick')
  11. process.nextTick(() => {
  12. console.log('nextTick')
  13. process.nextTick(() => {
  14. console.log('nextTick')
  15. })
  16. })
  17. })
  18. })
  19. // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

根据语言规格,Promise对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。
微任务队列追加在process.nextTick队列的后面,也属于本轮循环。
Node 中的 process.nextTick 会先于其他 microtask 执行

  1. setTimeout(() => {
  2. console.log("timer1");
  3. Promise.resolve().then(function() {
  4. console.log("promise1");
  5. });
  6. }, 0);
  7. process.nextTick(() => {
  8. console.log("nextTick");
  9. });
  10. // nextTick, timer1, promise1
  1. async function async1(){
  2. console.log('async1 start')
  3. await async2()
  4. console.log('async1 end')
  5. }
  6. async function async2(){
  7. console.log('async2')
  8. }
  9. console.log('script start')
  10. setTimeout(function(){
  11. console.log('setTimeout0')
  12. },0)
  13. setTimeout(function(){
  14. console.log('setTimeout3')
  15. },3)
  16. setImmediate(() => console.log('setImmediate'));
  17. process.nextTick(() => console.log('nextTick'));
  18. async1();
  19. new Promise(function(resolve){
  20. console.log('promise1')
  21. resolve();
  22. console.log('promise2')
  23. }).then(function(){
  24. console.log('promise3')
  25. })
  26. console.log('script end')
  27. script start
  28. async1 start
  29. async2
  30. promise1
  31. promise2
  32. script end
  33. nextTick
  34. async1 end
  35. promise3
  36. setTimeout0
  37. setImmediate
  38. setTimeout3
  39. node v10.16.0
  40. script start
  41. async1 start
  42. async2
  43. promise1
  44. promise2
  45. script end
  46. nextTick
  47. promise3
  48. async1 end
  49. setTimeout0
  50. setTimeout3
  51. setImmediate

浏览器和node事件循环区别

浏览器环境下,microtask微任务的任务队列是每个macrotask宏任务执行完之后执行。
Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务
image.png

  1. setTimeout(()=>{
  2. console.log('timer1')
  3. Promise.resolve().then(function() {
  4. console.log('promise1')
  5. })
  6. }, 0)
  7. setTimeout(()=>{
  8. console.log('timer2')
  9. Promise.resolve().then(function() {
  10. console.log('promise2')
  11. })
  12. }, 0)
  13. 浏览器端运行结果:timer1=>promise1=>timer2=>promise2
  14. task queue
  15. -----------
  16. micro[微任务]
  17. macro[宏任务] 定时器1 定时器2
  18. ------------
  19. 遇到两个定时器,放到宏任务队列中
  20. 栈为空,事件队列中取定时器1,打印timer1,将微任务放到对队列中,并执行打印promise1
  21. 取定时器2,打印timer2,将微任务放到微任务队列,并打印promise2
  22. nodev11:结果和浏览器一样
  23. nodev10及以前:timer1=>timer2=>promise1=>promise2
  24. task queue
  25. ----------
  26. timer
  27. micro task queue promise1 promise2
  28. poll
  29. micro task queue
  30. check
  31. ----------
  32. 执行定时1,打印timer1,将微任务1放到micro task queue
  33. 执行定时2,打印timer2,将微任务2放到micro task queue
  34. 执行micro task queue打印promide1 promise2

References

https://mp.weixin.qq.com/s/HhnO-CdFsZ6_AcPxmMyr9w
https://mp.weixin.qq.com/s/eOM4Img6XK_gqpu3M6yjBg
https://juejin.im/post/5cf25a19f265da1bba58ec43#heading-9
https://www.yuque.com/guanguan-ky3w9/ay2y2e/wndeoo#2Hg7R