简单理解Event Loop
由于JavaScript是单线程,所以在同一时间只能完成一件事情。打个比方,程序在进行大量计算或进行IO操作,是无法进行DOM的UI渲染的。
我们可以看看以下的伪代码
const val1 = $.get('/test1')
const val2 = $.get('/test2')
const val3 = $.get('/test3')
console.log(val1) // 1
console.log(val2) // 2
console.log(val3) // 3
我们假设JavaScript只支持同步顺序执行(网络延迟2s),那么上述代码就会是这样。
程序用6s进行网络请求操作(期间页面卡死,无法进行任何操作,UI无法渲染),然后在控制台中输出1 2 3。如果途中有一个请求一直没有回包,那么这个程序就一直卡死了。
真实中我们看到浏览器页面没有因为网络IO导致页面卡死,这是因为JavaScript通过组合使用执行栈和任务队列(task queue)解决异步问题。
顺序执行栈
首先我们先一起看一下栈调用。
function func1(n) {
console.log(n + 1);
}
function func2(m, callback) {
console.log(m + 1);
callback(m + 1);
}
function func3(m, n, callback) {
console.log(m + n);
callback(m + n);
}
func3(1, 2, (res) => {
func2(res, res2 => {
func1(res2);
});
});
程序很简单,输出
3
4
5
我们一起看看程序在栈中表现。
任务队列
接下来我们一起看看任务队列的概念,很简单的,哈哈。
function main() {
console.log(1);
setTimeout(function cb() {
console.log(3);
}, 1000);
console.log(2);
}
main();
结果也是显而易见的,在输出1和2,等待约1秒输出3
1
2
3
main进栈
console.log(1)进栈,控制台打印1后,console.log(1)出栈。
setTimeout(cb, 1000)进栈,发现setTimeout是浏览器api,因此把这段代码交由浏览器执行,1000ms后cb函数进入任务队列。
在setTimeout交由浏览器执行的时候,console.log(2)进栈,控制台打印2后,console.log(2)出栈,然后main函数执行完毕,main函数出栈。
此时执行栈为空,任务队列存在cb()函数。cb()函数被压入栈中执行,输出3后,console.log(3)和cb()依次弹出栈,程序执行完毕。
好,看到这里,把上面的代码的1000延时换成0,看看结果是什么。
function main() {
console.log(1);
setTimeout(() => {
console.log(3);
}, 0);
console.log(2);
}
main();
套用上面的分析我们可以很快说出答案
1
2
3
macro task 与 micro task
任务队列(task queue)可以分为两种
- 宏任务队列 macro task queue
- setTimeout
- setInterval
- requestAnimationFrame
- I/O
- UI rendering
- 微任务队列 micro task queue
- Promise
- Object.observe
- MutationObserver
在浏览器中,代码执行的顺序如下:
- 优先执行stack中的代码,遇到上述特殊的api,则放置到任务队列中。例如,promise则放置到micro task,setTimeout则放到macro task。
- 执行栈为空时,先查看micro task队列,若队列中存在任务,则从队列首部获取一个任务放入Stack中执行,队列长度减1。若此时又产生一个微任务,则继续放到队尾。
- 重复2的操作,直到micro task队列为空。
- 查看macro task,把队首任务放入Stack中执行。
- 重复4,直到Stack为空。
function func1() {
return new Promise((resolve, reject) => {
console.log(8);
resolve(9);
});
}
async function func2() {
const num = await func1();
console.log(num);
}
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
});
func2();
new Promise((resolve, reject) => {
console.log(4);
resolve(5);
}).then((data) => {
console.log(data);
});
setTimeout(() => {
console.log(6);
});
console.log(7);
套用上面的规则,测试一下能否得到正常的结果(注意,必须要在浏览器环境测试,node的结果会不一样的,因为node的event loop和浏览器的不太一样)
1
8
4
7
9
5
2
3
6