Advanced-Frontend/Daily-Interview-Question

第 25 题:浏览器和Node 事件循环的区别

Rain120 opened this issue · 17 comments

第 25 题:浏览器和Node 事件循环的区别

其中一个主要的区别在于浏览器的event loop 和nodejs的event loop 在处理异步事件的顺序是不同的,nodejs中有micro event;其中Promise属于micro event 该异步事件的处理顺序就和浏览器不同.nodejs V11.0以上 这两者之间的顺序就相同了.
参考一下系列文章:https://jsblog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3

为楼上补充一个例子

原文出自liubasara的个人博客

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

test()

// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

题目应该是:浏览器和node的事件循环的区别吧,

先上链接:

第一个链接里面大佬讲的已经非常透彻了我来总结一下。

浏览器

关于微任务和宏任务在浏览器的执行顺序是这样的:

  • 执行一只task(宏任务)
  • 执行完micro-task队列 (微任务)

如此循环往复下去

浏览器的task(宏任务)执行顺序在 html#event-loops 里面有讲就不翻译了
常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。

Node

Node的事件循环是libuv实现的,引用一张官网的图:

default

大体的task(宏任务)执行顺序是这样的:

  • timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
  • pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
  • check 检测:setImmediate() 回调函数在这里执行。
  • close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', ...)。

微任务和宏任务在Node的执行顺序

Node 10以前:

  • 执行完一个阶段的所有任务
  • 执行完nextTick队列里面的内容
  • 然后执行完微任务队列的内容

Node 11以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。

Node 官方文档从始至终到没有提到微任务和宏任务的概念。

之所以会出现 Node 10 和 Node 11 的异步执行差异,可以看官方文档 Deduplication 这一节内容,以及 Node 11 的 PR,TLDR 在 Node 11 之后是为了消除和浏览器执行的差异。

Promise.resolve().then(() => {console.log('children2-1')})

大佬,我的理解是:在11之前宏任务中有微任务的,会把微任务仍到微任务的队列,先执行了宏任务中的立即执行方法再去执行微任务。11以后执行宏任务的时候会把自己任务中所有的事儿(除了宏任务)都干完才会执行下个阶段任务,而不会将其中的微任务搁置到微任务队列。这么理解对么。。。

nodejs的api事件循环基本都是io多线程异步,而浏览器除了ajax外基本都是非阻塞异步模型

为楼上补充一个例子

原文出自liubasara的个人博客

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

test()

// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

实际测试,不管是不是node11还是11以下都是下面的结果
start
end
children1
children2
children2-1
children3
children3-1

function sleep(time) {    
	let startTime = new Date();    
	while (new Date() - startTime < time) {}    
	console.log('<--Next Loop-->');
}

setTimeout(() => {    
	console.log('timeout1');
    setTimeout(() => {        
    	console.log('timeout3');
        sleep(1000);
    });    
    new Promise((resolve) => {        
    	console.log('timeout1_promise');
        resolve();
    }).then(() => {        
    	console.log('timeout1_then');
    });
    sleep(1000);
});
     
setTimeout(() => {    
	console.log('timeout2');
    setTimeout(() => {        
    	console.log('timeout4');
        sleep(1000);
    });    
    new Promise((resolve) => {        
    	console.log('timeout2_promise');
        resolve();
    }).then(() => {       
    	console.log('timeout2_then');
    });
    sleep(1000);
});

浏览器下的结果
image

node下结果
timeout1
timeout1_promise
<--Next Loop-->
timeout2
timeout2_promise
<--Next Loop-->
timeout1_then
timeout2_then
timeout3
<--Next Loop-->
timeout4
<--Next Loop-->

浏览器的EventLoop是将所有的微任务执行完,再执行宏任务,再执行宏任务中所有的微任务,再执行宏任务,再执行宏任务中的所有微任务

node的时间循环是交叉执行,执行完同级的所有timer类, 再执行同级的所有promise.then,再执行所有的同级timer

function sleep(time) {    
	let startTime = new Date();    
	while (new Date() - startTime < time) {}    
	console.log('<--Next Loop-->');
}

宏任务1-setTimeout(() => {    
	console.log('宏任务timeout1-一级');
    宏任务3-setTimeout(() => {        
    	console.log('宏任务timeout1-二级');
        sleep(1000);
    });    
    new Promise((resolve) => {        
    	console.log('微任务promise1-同步');
        resolve();
    }).then(() => {        
    	console.log('微任务promise1-异步then');
    });
    sleep(1000);
});
     
宏任务2-setTimeout(() => {    
	console.log('宏任务timeout2-一级');
    宏任务4-setTimeout(() => {        
    	console.log('宏任务timeout2-二级');
        sleep(1000);
    });    
     new Promise((resolve) => {        
    	console.log('微任务promise2-同步');
        resolve();
    }).then(() => {       
    	console.log('微任务promise2-异步then');
    });
    sleep(1000);
});

浏览器执行结果:

  • 主程序中 两个宏任务,那就先执行 宏任务1 里的同步代码
    输出:
    宏任务timeout1-一级
    微任务promise1-同步
    <--Next Loop-->
    再执行宏任务1中的微任务promise.then代码
    输出:
    微任务promise1-异步then
    第一轮EventLoop结束!!!

  • 第二轮宏任务2开始执行同步代码:
    输出:
    宏任务timeout2-一级
    微任务promise2-同步
    <--Next Loop-->
    再执行宏任务2中的微任务promise.then代码
    输出:
    微任务promise2-异步then
    第二轮EventLoop结束!!!

  • 第三轮宏任务3开始执行同步代码,没有微任务代码
    输出:
    宏任务timeout1-二级
    <--Next Loop-->
    第三轮EventLoop结束!!!

  • 第四轮宏任务4开始执行同步代码,没有微任务代码
    输出:
    宏任务timeout2-二级
    <--Next Loop-->
    第四轮EventLoop结合!!!

// 浏览器输出结果
宏任务timeout1-一级
微任务promise1-同步
<--Next Loop-->
微任务promise1-异步then
宏任务timeout2-一级
微任务promise2-同步
<--Next Loop-->
宏任务timeout1-二级
<--Next Loop-->
宏任务timeout2-二级
<--Next Loop-->

node执行结果:

  • 先同级 宏任务timeout1 和 宏任务timeout2
    执行timeout1同步代码输出:
    宏任务timeout1-一级
    微任务promise1-同步
    <--Next Loop-->
    执行timeout2同步代码输出:
    宏任务timeout2-一级
    微任务promise2-同步
    <--Next Loop-->

  • 再执行同级 微任务promise1 和 promise2
    执行promise1代码输出:
    微任务promise1-异步then
    执行promise2代码输出:
    微任务promise2-异步then

  • 再执行同为2级的宏任务timeout3 和 timeout4
    执行timeout3输出:
    宏任务timeout1-二级
    <--Next Loop-->
    执行timeout4输出:
    宏任务timeout2-二级
    <--Next Loop-->

// node11以下执行
宏任务timeout1-一级
微任务promise1-同步
<--Next Loop-->
宏任务timeout2-一级
微任务promise2-同步
<--Next Loop-->
微任务promise1-异步then
微任务promise2-异步then
宏任务timeout1-二级
<--Next Loop-->
宏任务timeout2-二级
<--Next Loop-->

差异体现在nodeV10之前
浏览器是执行完一个宏任务就会去清空微任务队列;node则是将同源的宏任务队列执行完毕后再去清空微任务队列;
另外,宏任务内若嵌套同源宏任务,仍会放进一个队列,但是执行将会放在下一次事件循环;(举个例子,timeoutTwo中包含一个timeoutThree,timeoutThree仍会放进setTimeout队列,但并不会与one、two一起执行完毕,而是等到清空微任务队列的下一次循环时执行);
image

例子:

console.log(1);

setTimeout(() => {
    console.log(2)
    new Promise((resolve) => {
        console.log(6);
        resolve(7);
    }).then((num) => {
        console.log(num);
    })
});

setTimeout(() => {
    console.log(3);
       new Promise((resolve) => {
        console.log(9);
        resolve(10);
    }).then((num) => {
        console.log(num);
    })
    setTimeout(()=>{
    	console.log(8);
    })
})

new Promise((resolve) => {
    console.log(4);
    resolve(5)
}).then((num) => {
    console.log(num);
    new Promise((resolve)=>{
    	console.log(11);
    	resolve(12);
    }).then((num)=>{
    	console.log(num);
    })
})

答案:
左边为浏览器的执行结果,右边为node v8.12.0的执行结果
image

dmljc commented

@Liubasara Promise.resolve().then(() => {console.log('children1')}) 这个是微任务吧????怎么能说: 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)?????请指教??

为楼上补充一个例子

原文出自liubasara的个人博客

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

test()

// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

实际测试,不管是不是node11还是11以下都是下面的结果
start
end
children1
children2
children2-1
children3
children3-1

用n切版本的时候,刚切换完跑确实是这样的,我执行了一下node -v,显示确实切换成功了,再跑一次就变了

参考这篇文章浏览器与Node的事件循环(Event Loop)有何区别?,我有一个疑问

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

而下面这段代码的先后顺序就不确定了。疑问就在这里,按照推断,这部分的代码应该也是运行在poll阶段的,为什么他们顺序不确定?看了node官方文档也没有弄清楚,望哪位大神解惑。

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});

参考这篇文章浏览器与Node的事件循环(Event Loop)有何区别?,我有一个疑问

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

而下面这段代码的先后顺序就不确定了。疑问就在这里,按照推断,这部分的代码应该也是运行在poll阶段的,为什么他们顺序不确定?看了node官方文档也没有弄清楚,望哪位大神解惑。

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});

文章解释挺清楚了,setTimeout设置0回调其实并不能精确到0的,时间在0-1之间

这是由源码决定的 进入事件循环也是需要成本的,如果在准备时候花费了大于 setTimeout回调的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
如果准备时间花费小于,那么就是 setImmediate 回调先执行了

和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列

所以没区别了是吗