浏览器与Node的Event Loop详解

Event Loop是什么

event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

  • 浏览器的Event Loop是在html5的规范中明确定义。
  • NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
  • libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。

event loop过程

  1. 同步代码,一行一行放在Call Stack执行
  2. 遇到异步先记录下来,等待时机(定时,网络请求)
  3. 时机一到立马推入Callback Queue中
  4. 如Call Stack为空(同步代码执行完毕),Event Loop开始工作
  5. 轮询查找Callback Quque,有则移动到Call Stack中执行
  6. 然后继续轮询查找

执行顺序

  1. Call Stack清空
  2. 执行当前的微任务
  3. 尝试DOM渲染
  4. 触发Event Loop

微任务和宏任务的区别

  • 宏任务:DOM渲染后触发,由浏览器规定(Web APIs)
  • 微任务:DOM渲染前执行,微任务是ES6语法规定

event loop和Dom渲染

  • 每次Call Stack清空(即每次轮询结束),即同步任务执行完
  • 都是Dom重写渲染的机会,Dom结构如有改变则重新渲染
  • 然后再去触发下一次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

  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。

例子1:

    console.log(1);
      setTimeout(() => { // callback1
        console.log(2);
        Promise.resolve().then(() => { // callback4
          console.log(3);
        });
      });
      new Promise((resolve, reject) => {
        console.log(4);
        resolve(5);
      }).then(data => { // callback2
        console.log(data);
      });
      setTimeout(() => { //callback3
        console.log(6);
      });
      console.log(7);


最终打印结果:
1
4
7
5
2
3
6

分析:

流程:1.执行全局script代码

Step 1

console.log(1)

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

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: []

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]

Step 4

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

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

Step 5

console.log(7)

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

2.全局Script代码执行完了,进入下一个步骤,从microtask queue中依次取出任务执行,直到microtask queue队列为空。

Step 6

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

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

3.这里microtask queue中只有一个任务,执行完后开始从宏任务队列macrotask queue中取位于队首的任务执行

Step 7

console.log(2)

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

4.执行callback1的时候又遇到了另一个Promise,Promise异步执行完后在microtask queue中又注册了一个callback4回调函数

Step 8

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

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

5.取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行

Step 9

 console.log(3)

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

6.微任务队列全部执行完,再去宏任务队列中取第一个任务执行

Step 10

console.log(6)

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

7.以上,全部执行完后,Stack Queue为空,Macrotask Queue为空,Micro Queue为空

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

例子2:

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个,后面我们会讲到。

async/await执行顺序

async/await例子3:

    async function async1() {
        console.log("2");
        await async2();
        console.log("7");
      }
      async function async2() {
        console.log("3");
      }
      setTimeout(function() {
        console.log("8");
      }, 0);

      console.log("1");
      async1();
      new Promise(function(resolve) {
        console.log("4");
        resolve();
      }).then(function() {
        console.log("6");
      });
      console.log("5");

输出顺序

// 1 2 3 4 5 7 6 8

对这个例子做下改动,await后面跟一个promise

    async function async1() {
        console.log("2");
        await async2();
        console.log("7");
      }
      async function async2() {
        console.log("3");
        return new Promise(function(resolve) {
          console.log("4");
          resolve();
        }).then(function() {
          console.log("6");
        });
      }
      setTimeout(function() {
        console.log("8");
      }, 0);

      console.log("1");
      async1();
      console.log("5");

此时输出:

// 1 2 3 4 5 6 7 8

可见await后面是否为promise会影响执行顺序

async起什么作用:

async函数返回的是一个Promise对象,如果在函数return一个直接量,async会把这个直接量通过Promise.resolve()封装成Promise对象。

await到底在等啥:

await在等async函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。

await做了什么处理:

await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈的代码。等本轮事件循环执行完之后又会跳回到async函数中,等待await后面表达式的返回值,如果返回值为非Promise则继续执行async函数后面的代码,否则将返回的Promise放入Promise队列(Promise的Job Queue)。

await后面的内容相当于在.then执行。await返回如果不是一个promise对象,那么await后面就按正常js顺序执行,先执行同步代码,当主线程空闲了,再去执行异步队列的任务;如果await返回的是一个promise对象,那么要等待这个对象解析完成,如果没有resolve或者reject,那么后面的内容就不会执行;如果有resolve或者reject,那么后面的内容正常执行。

async function test() {
     console.log("async1");
     await new Promise((resolve, reject) => {
         resolve()
     });
     console.log('async2');
 };
 //等价于
 async function test() {
     console.log("async1");
     await new Promise((resolve, reject) => {
         resolve()
     }).then(() => console.log('async2'));
 };

 

async function fn2() {
    await 2
    console.log(2)
}
//等价于
async function fn2() {
    Promise.resolve(2).then(() => console.log(2))
}

所以上述 async/await例子3中,console.log("7") 等价于.then(console.log("7") 也就是放入微任务队列中;而后面例子await后面跟一个promise,需要先等promise解析完成,也就是 console.log("6") 会先放入微任务队列,console.log("7") 后放入微任务队列,先解析完成打印 6 在 7 前面

 

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

NodeJS中的宏队列和微队列

NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段,如下图:

各个阶段执行的任务如下:

  • 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会被放置在不同的微队列中。

大体解释一下NodeJS的Event Loop过程:

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
  3. 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2;Node 在新版本中,也是每个 Macrotask 执行完后,就去执行 Microtask 了,和浏览器的模型一致。
  4. Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ......
  5. 这就是Node的Event Loop

例子1

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');


// 低版本node(<11)
start
end
999
111
777
444
888
555
333
666
222

// 高版本node(>=11)

  start
  end
  999
  111
  444
  777
  888
  555
  666
  333
  222

以高版本node分析:

流程:

1.执行全局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]

IO Callback Queue: []

Check Queue: [callback5]

Close Callback Queue: []

微队列

Next Tick Queue: [callback9]

Other Microtask Queue: []

2.全局Script执行完了,开始依次执行微任务Next Tick Queue中的全部回调任务。此时Next Tick Queue中只有一个callback9,将其取出放入调用栈中执行,打印999。

宏队列

Timers Queue: [callback1, callback7]

IO Callback Queue: []

Check Queue: [callback5]

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

3.开始依次执行6个阶段各自宏队列中的所有任务,先执行第1个阶段Timers Queue中的所有任务,先取出callback1执行,打印111,callback1函数继续向下,依次把callback2放入Timers Queue中,把callback3放入Check Queue中,把callback4放入Next Tick Queue中,然后callback1执行完毕。

此时,各队列情况如下:

宏队列

Timers Queue: [callback7, callback2]

IO Callback Queue: []

Check Queue: [callback5, callback3]

Close Callback Queue: []

微队列

Next Tick Queue: [callback4]

Other Microtask Queue: []

4.此时,取出Next Tick Queue中的全部回调任务,执行callback4,打印444。

此时,各队列情况如下:

宏队列

Timers Queue: [callback7, callback2]

IO Callback Queue: []

Check Queue: [callback5, callback3]

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

5.Next Tick Queue执行完毕,开始执行Other Microtask Queue中的任务,因为里面为空,所以继续向下。先执行第1个阶段Timers Queue中的所有任务,先取出callback7执行,打印777,把callback8放入Next Tick Queue中,然后callback7执行完毕。

宏队列

Timers Queue: [callback2]

IO Callback Queue: []

Check Queue: [callback5, callback3]

Close Callback Queue: []

微队列

Next Tick Queue: [callback8]

Other Microtask Queue: []

6.此时,取出Next Tick Queue中的全部回调任务,执行callback8,打印888。

此时,各队列情况如下:

宏队列

Timers Queue: [callback2]

IO Callback Queue: []

Check Queue: [callback5, callback3]

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

7. 因为此时第一轮Timers Queue执行完毕,第2个阶段IO Callback Queue队列为空,跳过,第3和第4个阶段一般是Node内部使用,跳过,进入第5个阶段Check Queue。取出callback5执行,打印555,把callback6放入Next Tick Queue中。

此时,各队列情况如下:

宏队列

Timers Queue: [callback2]

IO Callback Queue: []

Check Queue: [callback3]

Close Callback Queue: []

微队列

Next Tick Queue: [callback6]

Other Microtask Queue: []

8.此时,取出Next Tick Queue中的全部回调任务,执行callback6,打印666,Next Tick Queue执行完毕,开始执行Other Microtask Queue中的任务,因为里面为空,所以继续向下

此时,各队列情况如下:

宏队列

Timers Queue: [callback2]

IO Callback Queue: []

Check Queue: [callback3]

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

9.取出Check Queue中的callback3执行,打印333,继续向下,因为队列都为空,进入第二轮循环

此时,各队列情况如下:

宏队列

Timers Queue: [callback2]

IO Callback Queue: []

Check Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

10.取出Timers Queue中的callback2执行,打印222,整个执行完毕

此时,各队列情况如下:

宏队列

Timers Queue: []

IO Callback Queue: []

Check Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

说明:

上面这段代码你执行的结果可能会有多种情况,原因解释如下。

  • 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中,结果就唯一了。

引入Promise再来看一个例子2:

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')
    })
})


// 高版本node(>=11)

  1
  7
  6
  8
  2
  4
  3
  5
  9
  11
  10
  12

// 低版本node(<11)
1 7 6 8 2 4 9 11 3 10 5 12

相比于上一个例子,这里由于存在Promise,所以Other Microtask Queue中也会有回调任务的存在,执行到微任务阶段时,先执行Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务,然后才会进入下一个阶段的宏任务。

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

第3点修改:
Node 在新版本中,也是每个 Macrotask 执行完后,就去执行 Microtask 了,和浏览器的模型一致。

参考:https://segmentfault.com/a/1190000016278115

posted @ 2021-04-13 17:39  盼星星盼太阳  阅读(303)  评论(0编辑  收藏  举报