背景
Event loop 是一个很重要的概念,本质上指的是计算机的运行机制,JavaScript语言采用的就是这种机制,众所周知JavaScript是单线程,为什么会设计成单线程呢?其实早在几年前阮一峰老师就给出了答案,这样的好处提升效率,同一个时间只做一件事
。但也导致了一个问题:就是所有的任务都需要排队,只有前面的任务执行结束,才能执行后面的任务。JavaScript语言的设计者意识到这样不行,于是就把所有的任务分为两种:同步任务
和异步任务
维护一个任务队列,主线程从任务队列中读取任务,整个过程是循环不断,这种机制称为Event loop 又叫 事件循环。
为什么需要了解它
在实际的工作中,了解Event loop能帮助你分析一个异步次序的问题,除此之外还能对你了解浏览器和Node的内部机制起到积极的作用,最主要的对于面试这是一个百分百会问到的问题。
浏览器的实现
浏览器中主要任务把分为两种: 同步任务、异步任务;
异步任务:Macrotask(宏任务)、Microtask(微任务)
,宏任务与微任务队列中的任务是随着:任务进栈、出栈、任务出队、进队之间交替进行。可以通过一个伪代码来了解一下这个概念:
// 任务队列(先进先出)
let EventLoop = [];
let event;
// “永远” 执行
while (true) {
// 一次tack
if (EventLoop.length > 0) {
// 拿到队列中的下一次事件
event = EventLoop.shift();
// 现在、执行下一个事件
try {
event();
} catch (error) {
// 报告错误
reportError(error);
}
}
}
常见的Macrotask(宏任务)
- script 标签
- setTimeout
- setInterval
- setImmediate (Node环境中)
- requestAnimationFrame
Microtask(微任务)
- process.nextTick (Node环境中)
- Promise callback 包括:()
- MutationObserver
知道概念后 我们看一个简单的例子入手,先不必知道最后执行的打印的结果,你应该要清楚当前的代码那些是宏任务
、微任务
栗子🌰
console.log('start'); // 编号1
setTimeout(function () { // 编号2
console.log('timeout');
}, 0);
Promise.resolve().then(function () { // 编号3
console.log('promise');
});
console.log('end'); // 编号4
实现的过程:
过程
:
- 运行时识别到了log方法将其入栈、然后执行输入
start
出栈 - 识别到了setTimeout为异步的方法(
宏任务
),把匿名回调函数放在(宏任务)队列中,在下一次事件循环中执行。 - 执行遇到
promise callback
、属于(微任务
),放在(微任务)队列中。 - 运行时识别到了log方法将其入栈、然后执行输入
end
出栈。 - 主进程执行完毕,栈为空,随即从(微任务)队列取出队首的项,打印
promise
、直到(微任务)队列没有数据 - 循环下一个(宏任务)队列,遵从先进先出的原则,打印出
timeout
任务的类型
- 编号1 : 同步任务
- 编号2 : 宏任务
- 编号3 : 微任务
- 编号4 : 同步任务
执行的结果:
start
end
promise
timeout
趁热打铁在来一个
console.log('start'); // 编号1
new Promise(function(resolve, rejected){
console.log('Promise-1') // 编号 2
resolve()
}).then(function(res){ // 编号 3
console.log('Promise-2')
})
setTimeout(function () { // 编号 4
console.log('timeout');
}, 0);
Promise.resolve().then(function () { // 编号5
console.log('promise');
});
console.log('end'); // 编号6
实现运行过程:
其实这个例子跟上面的的唯一区别就是增加了一个 new Promise
也就是编号2
打印console.log('Promise-1')
这个需要注意的是只有Promise callback
属于异步任务的(微任务
),但是在函数内部里面属于同步任务
,很多人常常在这里搞混。
结果:
start
Promise-1
end
Promise-2
promise
timeout
彻底解锁Event loop
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();
实现运行过程:
第一次事件循环:
解析整个JavaScript文件处于一个宏任务
中,遇到同步console.log('1')
直接打印。接着执行遇到function
但是没有进行调用直接跳过,来到第一个setTimeout
,塞进到(宏任务)的Queue标记为macro1
,接着解析到new Promise
执行里面的代码console.log('7')
,遇到then塞入到(微任务)Queue中标记micro1
,之后又遇到了setTimeout
再次塞入到(宏任务)的Queue标记为macro2
,最后到foo()
函数,变量提升执行foo函数的遇到async
只是标明当前的函数为异步,不影响函数的执行console.log('13')
,遇到awiat bar
执行bar的console.log('14')
,awiat与阻塞
后面的代码,放入微任务列表标记micro2
;
当前宏任务:
- 执行的同步代码为:
[1、7、13、14]
- 微任务Queue:
[8、15]
- 宏任务Queue:
[macro1、macro2]
此时输入的结果:1、7、13、14、8、15
输入完毕清空当前的微任务队列此时micro = []
第二次事件循环:
保持先进先出的形式、会执行第一个setTimeout
输出console.log('2')
,执行到new promise
输入到console.log('4')
、遇到then放在micro
中重新标记为micro1
,检查没有其他微任务的时候直接输出console.log('5')
当前宏任务:
- 执行的同步代码为:
[2、4]
- 微任务Queue:
[5]
- 宏任务Queue:
[macro2]
此时输入的结果:2、4、5
输入完毕清空当前的微任务队列此时micro = []
第三次事件循环:
跟上一次事件循环一样的顺序
当前宏任务:
- 执行的同步代码为:
[9、11]
- 微任务Queue:
[12]
- 宏任务Queue:
[]
此时输入的结果:9、11、12
输入完毕清空所有的任务队列
总结
在事件循环中分清什么是宏任务和微任务很关键、只有弄清楚了顺序才能知道当前事件执行的顺序,后面不管是在面试和工作中都会游刃有余。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。