事件循环和任务队列

如有错误欢迎指正,谢~

 

浏览器的Event Loop:目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲。

 

关于js事件循环机制,经常会在面试中被问到,究竟是怎么回事呢,让我们了解了解。

众所周知js是单线程,浏览器能够很好的处理异步请求,这事为什么呢?

浏览器是多进程的,浏览器的每个tab标签都是独立的进程,其中浏览器渲染进程(l浏览器内核)属于浏览器多进程的一种,主要负责页面的渲染,脚本执行,事件处理等。

其中包含的线程有:GUI渲染线程(负责页面渲染,解析HTML,css 构成dom树),js引擎线程,事件触发线程,定时器触发线程,http请求线程等主要线程。

执行中的线程:

主线程:就是js引擎线程,这个线程只有一个,页面渲染和函数处理在这个主线程上面工作。

工作线程:与主线程分开的线程,处理文件读取,网络请求等异步事件

 

任务队列:

所有任务可以分为同步任务和异步任务,同步任务,就是立即执行的任务,同步任务一般会直接进入主线程执行,而异步任务就是异步执行的任务,像ajax请求,settimeout等函数是异步任务,异步任务会通过任务队列机制,先进先出来进行执行。先看一个图:

 

 

 同步任务和异步任务会进入不同的执行环境,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程的任务执行完毕为空,会去任务队列读取任务,进入主线程执行,上述过程不断重复,我们称为事件循环(Event Loop)。

在每次事件循环经历如下步骤:

1,在 此次循环中,选择最先进入队列的,如果有则执行一次

2,检测是否有Miscrotasks ,如果不停的执行,直至微任务队列为空。

3,更新render.

4,主线程重复执行上面步骤。

macrotasks(宏任务)(就是我们平常说的任务队列)和microtasks(微任务)的划分:

macrotasks包括:script (整体代码),I/O,UI交互事件,setTimeout,setInterval,setImmediate(node.js环境独有)。

microtasks包括:Promises,Object.observe(已废弃),MutationObserver(h5新增),process.nextTick(node.js环境独有)。

 

对于async/await 说明一下执行顺序

async  声明的函数的返回本质是promise,await用于async函数内部,在等待一个异步返回接口,await等待过程中会跳出async,让出线程(阻塞await后面的代码执行),去执行async函数同级的代码,而await后面的代码属于异步代码,放入宏观任务等待执行。

趁热看个例子:

 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("settimeout");
12 }, 0);
13 async1();
14 new Promise(function (resolve) {
15     console.log("promise1");
16     resolve()
17 }).then(function () {
18     console.log("promise2");
19 })
20 console.log("script end");

 

让我们分析一下:

首先执行栈执行同步任务,先输出script start,之后遇到settimeout把回调函数放到宏观任务队列等待执行,遇到async1调用,执行同步任务输出async1 start,遇到await 调用async2函数,直接输出async2,而await后面的async1 end 放到宏任务队列中等待执行,接下来直接运行new promise 中同步代码输出promise1,然后resove,遇到then,异步微任务,把then回调函数放到微任务队列等待执行,再输出同步任务script end,执行栈为空了。去查看微任务队列有没有等待执行的代码,一看有个then回调等待执行呢,输出promise2,执行完全部的微任务队列,进行下一次循环,执行宏任务队列,输出async1 end,再看微任务队列有没有任务,一看没有,进行下一次循环,再看宏观任务队列有没有任务,最后一个宏观任务,输出settimeout。执行完毕。

//最后结果

script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout

 再来说一下微任务队列中还分两种优先级队列,其中就是process.nextTickt微任务队列和promise.then微任务队列,在执行微任务队列的时候,process.nextTick队列优先级高于promise.then队列,优先执行全部的process.nextTick,再执行promise.then。

 再看一下例子:

 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("settimeout");
12 }, 0);
13 async1();
14 new Promise(function (resolve) {
15     console.log("promise1");
16     resolve()
17 }).then(function () {
18     console.log("promise2");
19 })
20 process.nextTick(function(){
21     console.log('process nextTick1')
22     process.nextTick(function(){
23         console.log('process nextTick2')
24         
25     })
26 })
27 
28 console.log("script end");

 

//最后结果

script start
async1 start
async2
promise1
script end
process nextTick1
process nextTick2
promise2
async1 end
settimeout

 

 

Node.js的Event Loop

所以与浏览器Event Loop也是不一样的。Node的Event Loop是分阶段的,如下图所示:

 

(1)timers: 执行setTimeout和setInterval的回调

(2)pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调

(3)idle, prepare: 仅系统内部使用

(4)poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。

(5)check: setImmediate在这里执行

(6)close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)


   还有个需要注意的是poll阶段,他后面并不一定每次都是check阶段,poll队列执行完后,如果没有setImmediate但是有定时器到期,他会绕回去执行定时器阶段

setImmediatesetTimeout:

上面的这个流程说简单点就是在一个异步流程里,setImmediate会比定时器先执行。

 1 console.log('outer');
 2 
 3 setTimeout(() => {
 4   setTimeout(() => {
 5     console.log('setTimeout');
 6   }, 0);
 7   setImmediate(() => {
 8     console.log('setImmediate');
 9   });
10 }, 0);

//运行结果:

outer
setImmediate
setTimeout

(1)外层是一个setTimeout,所以执行他的回调的时候已经在timers阶段了
(2)处理里面的setTimeout,因为本次循环的timers正在执行,所以他的回调其实加到了下个timers阶段
(3)处理里面的setImmediate,将它的回调加入check阶段的队列
(4)外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
(5)到了check阶段,发现了setImmediate的回调,拿出来执行
(6)然后是close callbacks,队列时空的,跳过
(7)又是timers阶段,执行我们的console

但是请注意我们上面console.log('setTimeout')console.log('setImmediate')都包在了一个setTimeout里面,如果直接写在最外层会怎么样呢?代码改写如下:

console.log('outer');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

outer
setImmediate
setTimeout

有时是:

outer
setTimeout
setImmediate

我们顺着之前的Event Loop再来理一下。在理之前,需要告诉大家一件事情,

node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1),这在官方文档中有说明。(说到这里顺便提下,HTML 5里面setTimeout最小的时间限制是4ms)。我们来理一下流程:

(1)外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
(2)遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
(3)遇到setImmediate塞入check阶段
(4)同步代码执行完毕,进入Event Loop
(5)先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
(6)跳过空的阶段,进入check阶段,执行setImmediate回调

 

再来了解一下process.nextTick

先看一个例子:

 1 process.nextTick(function(){
 2     console.log('process nextTick1')
 3     process.nextTick(function(){
 4         console.log('process nextTick2')
 5         
 6     })
 7 })
 8 setTimeout(()=>{
 9     console.log('timout')
10 },0)

//结果是:

process nextTick1
process nextTick2
timout

由于process.nextTick方法指定的回调函数,总是在执行栈的尾部触发,如果有多个process.nextTick语句(不管是否嵌套),将全部在当前’执行栈‘执行。

 

掌握了概念就看例子吧:

1 setTimeout(function(){
2     setImmediate(function(){
3         console.log('setImmediate2')
4     })
5     setTimeout(()=>{
6         console.log('timeout')
7     },0)
8 },0)

setImmediate2
timeout

 1 setTimeout(function(){
 2     // console.log('setImmediate1')
 3     setImmediate(function(){
 4         console.log('setImmediate2')
 5         setImmediate(function(){
 6             console.log('setImmediate3')
 7         })
 8     })
 9     setTimeout(()=>{
10         console.log('timeout')
11     },0)
12 },0)

setImmediate2
timeout
setImmediate3

 

 

 1 setTimeout(function(){
 2     console.log(1)
 3 },0);
 4 new Promise(function(resolve){
 5     console.log(2)
 6     for( var i=100000 ; i>0 ; i-- ){
 7         i==1 && resolve()
 8     }
 9     console.log(3)
10 }).then(function(){
11     console.log(4)
12 });
13 console.log(5);

结果是

2,3,5,4,1

 

eg2:

 1 setTimeout(function(){
 2     console.log(1)
 3 },0);
 4 setTimeout(function(){
 5     console.log(6)
 6 },2000);
 7 new Promise(function(resolve){
 8     console.log(2)
 9     for( var i=100000 ; i>0 ; i-- ){
10         i==1 && resolve()
11     }
12     console.log(3)
13 }).then(function(){
14     console.log(4);
15     setTimeout(function(){
16         console.log(8)
17     },1000)
18 });
19 console.log(5);

这个执行结果是啥呢???想想

结果是:

2
3
5
4
1
8
6

 

 eg3:

setImmediate(function(){
  console.log(1);
},0);
setTimeout(function(){
  console.log(2);
},0);
new Promise(function(resolve){
  console.log(3);
  resolve();
  console.log(4);
}).then(function(){
  console.log(5);
});
console.log(6);
process.nextTick(function(){
  console.log(7);
});
console.log(8);

node 环境下:

3
4
6
8
7
5
2
1

 eg4:

//加入两个nextTick的回调函数
process.nextTick(function () {
  console.log('nextTick延迟执行1');
});
process.nextTick(function () { 
  console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
  console.log('setImmediate延迟执行1'); 
  // 进入下次循环 
  process.nextTick(function () {
    console.log('强势插入');
  });
});
setImmediate(function () {
  console.log('setImmediate延迟执行2'); 
});
 
console.log('正常执行');

node环境:

正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
setImmediate延迟执行2
强势插入

 

 

总结一下,便于自己学习,也分享大家一起学习。(#^.^#)

posted @ 2019-07-02 23:17  JadeZhy  阅读(711)  评论(0编辑  收藏  举报