第 25 题:浏览器和Node 事件循环的区别
Rain120 opened this issue · 17 comments
其中一个主要的区别在于浏览器的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实现的,引用一张官网的图:
大体的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);
});
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一起执行完毕,而是等到清空微任务队列的下一次循环时执行);
例子:
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);
})
})
@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 回调先执行了
目前看到对 event loop 描述最清晰的系列文章:
来自 Deepal Jayasekara 大佬:
- Event Loop and the Big Picture — NodeJS Event Loop Part 1 翻译👉 Node事件循环系列——1、 事件循环总览
- Timers, Immediates and Process.nextTick— NodeJS Event Loop Part 2 翻译👉 Node事件循环系列——2、Timer 、Immediate 和 nextTick
- Promises, Next-Ticks, and Immediates— NodeJS Event Loop Part 3 翻译👉 Node事件循环系列——3、Promises, NextTicks 和 Immediates
- Handling IO — NodeJS Event Loop Part 4 翻译👉 Node事件循环系列——4、I/O的处理
- Event Loop Best Practices — NodeJS Event Loop Part 5 翻译👉 Node事件循环系列——5、事件循环的最佳实践
- New Changes to the Timers and Microtasks in Node v11.0.0 ( and above) 翻译👉 Node v11.0.0 中 Timers 和 Microtasks 的新变化
- JavaScript Event Loop vs Node JS Event Loop
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列
所以没区别了是吗