这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
前言
TIP ? 首先javaScript这门语言是一门单线程非阻塞的脚本语言。
单线程: JS引擎是基于单线程事件循环的概念构建的。同一时刻只运行一个代码块在执行,与之相反的是像JAVA和C++等语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不出错。
非阻塞: 当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,比如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞通过事件循环机制Event Loop实现。
Event Loop是什么?
Event Loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
宏队列和微队列
浏览器中主要任务把分为两种: 同步任务、异步任务;
异步任务:Macrotask(宏任务)、Microtask(微任务)
,宏任务与微任务队列中的任务是随着:任务进栈、出栈、任务出队、进队之间交替进行。
常见的Macrotask(宏任务)
- setTimeout
- setInterval
- setImmediate(node独有)
- requireAnimationFrame请求动画帧(浏览器独有)
- UI rendering(浏览器独有)
- I/O
Microtask(微任务)
- Process.nextTick(node独有,同步任务执行完就会执行process.nextTick的任务队列,process.nextTick优于Promise.then)
- Promise.then()
- Object.observe
- MutationObserve
浏览器中的Event Loop机制解析
在浏览器的事件循环中,首先要认清楚 3 个角色:函数调用栈、宏任务 ( macro-task) 队列和微任务 (micro- task) 队列。
当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈。后面每遇到一个函数调用,就会往栈中压入一个新的函数上下文。JS引擎会执行栈顶的函数,执行完毕后,弹出对应的上下文:
如果你有一堆需要执行的逻辑,它首先需要被推入函数调用栈,后续才能被执行。
循环过程:
- 执行全局Script的同步代码;
- 检查Microtask queues是否存在执行回调,有就执行microtask任务,直至全部执行完成,任务队列执行栈清空后进入下一步,例如peomise.then().then()两个微任务是依次执行的;
- 开始执行macrotask宏任务,Task Queues中按顺序取task执行,每执行完一个task都会检查Microtask队列是否为空(执行完一个Task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有Microtask,然后再进入下一个循环从Task Queue中取下一个Task执行,以此类推。
来一段代码实例:
console.log('1');
async function foo() {
console.log('13');
await bar();
console.log('15');
}
function bar() {
console.log('14');
}
setTimeout(function () {
console.log('2');
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');
});
setTimeout(function () {
console.log('9');
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12');
});
});
foo();
// 1、7、13、14、8、15、2、4、5、9、11、12
复制代码
第一次事件循环:
当前宏任务:
- 执行的同步代码为:
[1、7、13、14]
- 微任务Queue:
[8、15]
- 宏任务Queue:
[macro1、macro2]
此时输入的结果:
1、7、13、14、8、15
第二次事件循环:
当前宏任务:
- 执行的同步代码为:
[2、4]
- 微任务Queue:
[5]
- 宏任务Queue:
[macro2]
此时输入的结果:
2、4、5
第三次事件循环:
当前宏任务:
- 执行的同步代码为:
[9、11]
- 微任务Queue:
[12]
- 宏任务Queue:
[]
此时输入的结果:
9、11、12
Node中的Event Loop
node中Event Loop是由libuv实现的。
NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段,如下图:
各个阶段执行的任务如下:
- timers阶段:这个阶段执行setTimeout和setInterval中定义的回调
- I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks(少见,略过)
- idle, prepare阶段:仅node内部使用(可以略过)
- poll阶段:执行I/O回调,同时还会检查定时器是否到期
- check阶段:执行setImmediate()设定的回调
- close callbacks阶段:处理一些“关闭”的回调,执行socket.on(‘close’, ….)这些callbacks
NodeJS的Event Loop过程:
- 执行全局Script的同步代码
- 执行microtask微任务,优先清空next-tick队列中的任务,随后才会清空其他微任务;
- 开始执行macrotask宏任务,每次会尝试清空当前阶段对应宏任务队列里的所有任务;
- 步骤3开始,会进入3 -> 2 -> 3 -> 2 ….循环
循环形式: 宏任务队列 -> 微任务队列 -> 宏任务队列 -> 微任务队列 这样交替进行。
console.log('start');
setTimeout(() => { // callback1
console.log(1);
setTimeout(() => { // callback2
console.log(2);
}, 0);
setImmediate(() => { // callback3
console.log(3);
})
process.nextTick(() => { // callback4
console.log(4);
})
}, 0);
setImmediate(() => { // callback5
console.log(5);
process.nextTick(() => { // callback6
console.log(6);
})
})
setTimeout(() => { // callback7
console.log(7);
process.nextTick(() => { // callback8
console.log(8);
})
}, 0);
process.nextTick(() => { // callback9
console.log(9);
})
console.log('end');
// start end 9 1 7 4 8 5 3 6 2
复制代码