Event Loop曾经的理解
首先,JS是单线程语言,也就意味着同一个时间只能做一件事,那么
- 为什么JavaScript不是多线程呢?这样还能提高效率啊
假定JS同时有两个线程,一个线程在某个DOM节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢?
所以,设计者把JS设计成单线程应该就很好理解了,为了避免类似上述操作的复杂性,这一特征将来也不会变。
但是单线程有一个问题:一旦这个线程被阻塞就无法继续工作了,这肯定是不行的
由于异步编程可以实现“非阻塞”的调用效果,引入异步编程自然就是顺理成章的事情了,那么
- JS单线程如何实现异步的呢?
今天的主咖登场——事件循环(Event Loop),JS异步是通过的事件循环实现的,理解了Event Loop机制,就理解
了JS的执行机制。
先来段代码:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
for(let i = 3; i < 10000; i++){
console.log(i)
}
执行结果:1 3 4 5 6 7 ... 9997 9998 9999 2
setTimeout里的函数并没有立即执行,我们都知道这部分叫异步处理模块,延迟了一段时间,满足一定条件后才执行
仔细想想,我们在JS里通常把任务分为“同步任务”和“异步任务”,它们有以下的执行顺序:
- 判断任务是同步的还是异步的,如果是同步任务就进入主线程执行栈中,如果是异步任务就进入Event Table并注册函数,当满足触发条件后,进入Event Queue
- 只有等到主线程的同步任务执行完后,才会去Event Queue中查找是否有可执行的异步任务,如有,则进入主线程执行
以上两步循环执行,就是所谓的Event Loop,所以上述代码里:
console.log(1) 是同步任务,进入主线程,立即执行
setTimeout 是异步任务,进入Event Table,0ms后(实际时间可能有出入,见注文)进入Event Queue,等待进入主线程
for 是同步任务,进入主线程,立即执行
所有主线程任务执行完后,setTimeout从Event Queue进入主线程执行
*注:HTML5规范规定最小延迟时间不能小于4ms,即x如果小于4,会被当做4来处理。 不过不同浏览器的实现不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms
这就是我之前对Event Loop的理解,但是自从看了这篇文章深入理解JS引擎的执行机制颠覆了我对Event Loop认识三观,看下面的代码
Event Loop现在的理解
console.log("start")
setTimeout(()=>{
console.log("setTimeout")
}, 0)
new Promise((resolve)=>{
console.log("promise")
resolve()
}).then(()=>{
console.log("then")
})
console.log("end")
尝试按照我们上面的JS执行机制去分析:
console.log("start")是同步任务,进入主线程,立即执行 setTimeout是异步任务,进入Event
Table,满足触发条件后进入Event Queue
new Promise是同步任务,进入主线程,立即执行
.then是异步任务,进入Event Table,满足触发条件后进入Event Queue,排在Event Queue队尾 console.log("end")是同步任务,进入主线程,立即执行
所以执行结果是:start > promise > end > setTimeout > then
But但是,亲自跑了代码结果却是:start > promise > end > then > setTimeout
对比结果发现,难道Event Queue里面的顺序不是队列的先进先出的顺序吗?还是这块执行时有什么改变,事实就是,前面按照同步和异步任务划分的方式并不准确,那么怎么划分才是准确的呢,先看图(转自谷雨JavaScript 异步、栈、事件循环、任务队列):
咣咣咣~敲黑板,知识点,知识点,知识点:
Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)
宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?
宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
微任务:process.nextTick, Promise的then或catch, Object.observer, MutationObserver.
那么结合上面的流程图和最初理解的执行机制,总结了一下更为准确的JS执行机制:
- 取且仅取一个宏任务来执行(第一个宏任务就是script任务)。执行过程中判断是同步还是异步任务,如果是同步任务就进入主线程执行栈中,如果是异步任务就进入异步处理模块,这些异步处理模块的任务当满足触发条件后,进入任务队列,进入任务队列后,按照宏任务和微任务进行划分,划分完毕后,执行下一步。
- 如果微任务队列不为空,则依次取出微任务来执行,直到微任务队列为空(即当前loop所有微任务执行完),执行下一步。
- 进入下一轮loop或更新UI渲染。
Event Loop就是循环执行上面三步,接下来使用上面的结论分析个例子帮助理解
- 微任务里嵌套宏任务
console.log('第一轮');
setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout1
console.log('第二轮');
Promise.resolve().then(() => { //为了便于叙述时区分,标记为 then1
console.log('A');
})
}, 0);
setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout2
console.log('第三轮');
console.log('B');
}, 0);
new Promise((resolve)=>{ //为了便于叙述时区分,标记为 Promise1
console.log("C")
resolve()
}).then(() => { //为了便于叙述时区分,标记为 then2
Promise.resolve().then(() => { //为了便于叙述时区分,标记为 then3
console.log("D")
setTimeout(() => { //为了便于叙述时区分,标记为 setTimeout3
console.log('第四轮');
console.log('E');
}, 0);
});
});
执行结果:第一轮 > C > D > 第二轮 > A > 第三轮 > B > 第四轮 > E
分析:
loop1:
第一步:首先执行全局宏任务,里面同步任务有下面两个,都立即进入主线程执行完后出栈
1.console.log('第一轮')
2.Promise1
输出 “第一轮” > “C”
异步任务有三个,分别进入相应的任务队列:
1.setTimeout1,该任务按照划分标准是 宏任务
setTimeout(() => {
console.log('第二轮');
Promise.resolve().then(() => {
console.log('A');
})
}, 0);
2.setTimeout2,该任务按照划分标准是 宏任务
setTimeout(() => {
console.log('第三轮');
console.log('B');
}, 0);
3.then2,该任务按照划分标准是 微任务
.then(() => {
Promise.resolve().then(() => {
console.log("D")
setTimeout(() => {
console.log('第四轮');
console.log('E');
}, 0);
});
});
所以此时宏任务队列为:setTimeout1,setTimeout2
微任务队列为:then2
第二步:loop1 微任务队列不为空,then2
出队列并执行,然后这个微任务里的 then3
继续进入微任务队列 ,setTimeout3
进入宏任务队列队尾
那么此时微任务队列为:then3
宏任务队列为:setTimeout1,setTimeout2,setTimeout3
但是此时第二步并没有完,因为微任务队列只要不为空,就一直执行当前loop的微任务,所以从微任务队列取出 then3
执行输出 “D”
此时微任务队列为:空
宏任务队列为:setTimeout1,setTimeout2,setTimeout3
到目前为止,当前loop的微任务对列为空,进入下一个loop,输出情况是“第一轮” > “C” > “D”
loop2:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout1
执行输出“第二轮”
,并then1
进入微任务队列
此时微任务队列为:then1
宏任务队列为:setTimeout2,setTimeout3
第二步:loop2 微任务队列不为空,则从微任务队列取出then1
执行,输出“A”
此时微任务队列为:空
宏任务队列为:setTimeout2,setTimeout3
到目前为止,当前loop的微任务对列为空,进入下一个loop,输出情况是“第一轮” > “C” > “D” > “第二轮” > “A”
loop3:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout2
执行输出“第三轮” > “B”
此时微任务队列为:空
宏任务队列为:setTimeout3
第二步:由于loop3 微任务队列为空,则直接进入下一轮loop,输出情况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B”
loop4:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout3
执行输出“第四轮” > “E”
此时微任务队列为:空
宏任务队列为:空
第二步:由于loop4 微任务队列为空,宏任务队列也为空,则此次Event Loop结束,最终输出情况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B” > “第四轮” > “E”
上面的整个过程就是更为准确的Event Loop,下面还有个不同的例子供读者自行尝试
- 宏任务里嵌套微任务
console.log('第一轮');
setTimeout(() => {
console.log('第二轮');
Promise.resolve().then(() => {
console.log('A');
})
}, 0);
setTimeout(() => {
console.log('第三轮');
console.log('B');
}, 0);
new Promise((resolve) => {
console.log("C")
resolve()
}).then(() => { //注意,这个函数改动啦
setTimeout(() => {
console.log('第四轮');
console.log('E');
Promise.resolve().then(() => {
console.log("D")
});
}, 0);
});
执行结果:第一轮 > C > 第二轮 > A > 第三轮 > B > 第四轮 > E > D
Links:
深入理解JS引擎的执行机制
JavaScript 异步、栈、事件循环、任务队列
JavaScript 运行机制详解:深入理解Event Loop
JavaScript:并发模型与Event Loop
JavaScript 运行机制详解:再谈Event Loop[阮一峰]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。