面试被问到了JavaScript的事件循环。
答:JavaScript先执行代码中的同步部分,然后从任务循环里读取任务。这种读取执行的循环就是任务循环。
又问:宏任务和微任务有什么区别。
答:。。。
本着面试题目广而不深的原则看了一些面试题,都只是浅尝辄止的扫过,并没有深入理解。今天花了点时间看了一些eventloop的资料。学习需要翻阅很多资料,集众家之长。再结合自己的思考。像前端大佬阮一峰的技术博客关于eventloop,2013年版的他后来也承认是自己的理解有错误,后面重新写了一篇,但是后来重新写的一篇竟然没有对宏任务和微任务的解释。对此,小白表示不能理解。
首先贴上我觉得比较好的一篇技术博客。
https://segmentfault.com/a/1190000016278115
自我总结。
浏览器和node中的JavaScript对eventloop的实现方式相似但不相同。
EventLoop是一个技术模型在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
宏队列和微队列
宏队列
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微队列
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
那么浏览器对JavaScript代码的执行流程如下
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
我觉得这个流程是理解这篇博客的核心,理解了这个流程任何代码的输出均信手拈来。(至少我是这样)
我对流程的理解如下
- 执行代码的同步部分
- 把微任务队列里的任务全部执行
- 执行宏任务的第一个任务(执行完有可能产生微任务),如果产生了微任务,将微任务推进微任务队列
- 重复2-4
这个重复的过程,就是我理解的eventloop。
理解了这个,那么任何代码的执行顺序都不再是问题
示例
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);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
如果要执行这段代码,我们首先假设自己是一个浏览器。
(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)
执行第一步
console.log(1);
主线程 | 宏队列 | 微队列 |
console.log(1) | 空 | 空 |
输出 1
执行第二步
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
主线程 | 宏队列 | 微队列 |
第一个setTimeout | console.log(2); Promise.resolve().then(() => { console.log(3) }); | 空 |
优先执行同步部分,setTimeout是宏任务所以这里不会有任何输出,宏任务我把它的回调函数放到宏队列,至此,输出仍然是1,
执行第三步
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
主线程 | 宏队列 | 微队列 |
Promise | console.log(2); Promise.resolve().then(() => { console.log(3) }); | promise |
promise是微任务,我把它放进微队列,这里有一个注意点,promise的构造部分是同步代码,所以会执行。此时的输出为1,4
执行第四步
setTimeout(() => {
console.log(6);
})
主线程 | 宏队列 | 微队列 |
第二个setTimeout | console.log(2); Promise.resolve().then(() => { console.log(3) }); | promise |
console.log(6); |
执行到第二个setTimeOut,宏任务,我把它的回调函数放进宏队列。
执行第五步
console.log(7);
主线程 | 宏队列 | 微队列 |
console.log(7) | console.log(2); Promise.resolve().then(() => { console.log(3) }); | promise |
第二个setTimeout的回调 |
这是同步代码,直接执行。此时的输出为1,4,7,好了,同步代码执行完毕,按照流程,我要执行微队列里的任务了,我要把微队列的任务一次拿到主线程中执行。因为众所周知,线程才是执行任务的基本单位。此时任务变成这样
主线程 | 宏队列 | 微队列 |
promise | console.log(2); Promise.resolve().then(() => { console.log(3) }); | 空 |
console.log(6); |
执行promise
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
promise执行resolve(5),console.log(5),此时输出变成1,4,7,5,主线程执行完promise之后空了,这时候微队列也空了,读取宏队列的第一个任务。也就是第一个setTimeout的回调
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
主线程 | 宏队列 | 微队列 |
console.log(2) | console.log(6); | Promise.resolve().then(() => { console.log(3) }); |
执行主线程的代码,输出变成1,4,7,5,2,主线程空了,执行微队列
主线程 | 宏队列 | 微队列 |
Promise.resolve().then(() => { console.log(3) }); | console.log(6); | 空 |
输出变成1,4,7,5,2,3,执行之后主线程和微队列都空了,再读取宏队列,console.log(6)
主线程 | 宏队列 | 微队列 |
console.log(6); | 空 | 空 |
执行主线程中的任务输出变成1,4,7,5,2,3,6。至此三个队列都空了。浏览器工作顺利完成!我们看看真实情况的输出。
膨胀了之后来一个进阶题
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,再执行微队列里的Promise,输出5,6,7,依次执行三个setTimeout,第一个输出2,3,第二个输出8.第三个输出9。
全部输出为1,4,10,5,6,7,2,3,8,9。
Node中的EventLoop
node中的Eventloop相对而言比浏览器端复杂一点点。因为有node中才可以使用的Process.nextTick
简而言之,node中的微队列有两个,一个是Next Tick Queue微队列,它是放Process.nextTick的回调,还有一个Other Microtask Queue,它是放promise的。
node在执行的时候,首先会检查Next Tick Queue,再检查Other Microtask Queue,然后就和浏览器端的差不多。
总结代码的执行顺序
在Node.js中
代码同步部分--->Process.nextTick()--->promise--->setTimeout--->setInterval--->setImmediate
老规矩 代码测试
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')
})
})
同步部分1,process.nextTick输出6,promise输出7,8,第一个setTimeout输出2,4,3,5,第二个promise输出9,11,10,12
最终输出1,6,7,8,2,4,3,5,9,11,10,12
perfect!