前言
本篇文章主要讲解浏览器中事件循环(Event Loop) 那些事
单线程 JavaScript 中的同步和异步
上篇文章我们有讲到同步和异步的概念,实际上,为了实现异步模式,单线程的 JS 把所有任务都分为两种:同步任务和异步任务
同步任务是立即执行的任务,在调用栈(Call Stack)顺序执行
异步任务则不同,它在同步任务没完成之前,不会进入主线程,而是将对应回调函数注册到队列中,要理解这一步,我们先要知道任务队列
任务队列
在调用栈(Call Stack)中,如果遇到一个异步操作,那么会将对应的回调函数注册到任务队列,并且,任务队列会遵循先进先出的原则
不同的异步操作会进入到不同的任务队列中
任务队列在一贯的说法中,会细分为微任务(Micro Task)和宏任务(Macro Task),并且微任务的优先级会比宏任务高
尽管当前W3C最新标准中,已无宏任务的概念,而是用微任务队列、交互队列、延时队列等...,但目前使用微/宏任务概念来理解也并无问题
常见的微任务: Promise.then
、Promise.catch
、MutaionObserver
常见宏任务:setTimeout
、setInterval
、I/O 操作、DOM 事件、script
标签
注意,每个宏任务执行时,会先完整执行其所有同步代码,然后清空当前微任务队列(包括嵌套产生的微任务),最后才会处理下一个宏任务
图解事件循环机制
事件循环是用来处理异步任务的核心机制
用一句话来概括就是,在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行,这个运作流程机制就被称为事件循环(Event Loop)
- 所有同步代码直接进入调用栈,按顺序执行,直到调用栈清空
- 调用栈清空后,查找任务队列是否有任务
- 如果有,遵循先进先出的规则将最老的回调(最先进入任务队列的回调)推入栈中执行
- 重复上述流程,形成事件循环
第二、三步中,我们说会查找任务队列是否有任务并推入执行,由于微任务队列有很高的优先级,所以查找的顺序展开来说是:
- 优先检查微任务队列,如果队列不为空,遵循先进先出的规则推入栈中执行
- 当微任务队列清空后,当前事件循环就走完了
- 然后从宏任务中取出一个任务(最先进入的),推入调用栈执行,就进入下一轮循环
图示
理论总是抽象的,我们来举个实际的例子,你可以先思考一下这段代码的输出顺序
console.log('task1')
setTimeout(()=>{
new Promise((resolve,reject)=>{
console.log('task2')
resolve()
}).then(()=>{
console.log('task4')
}).then(()=>{
console.log('task7')
})
},0)
new Promise((resolve,reject)=>{
console.log('task3')
resolve()
}).then(()=>{
console.log('task6')
})
console.log('task5')
逐步分析这段代码:
第一轮事件循环(宏任务1: script):
同步任务
- 执行
console.log('task1')
,输出task1
- 遇到
setTimeout
,当time
时间结束时将其回调函数注册并放入宏任务队列 - 执行
new Promise
中的执行器函数,输出task3
- 执行器函数中的
resolve
方法执行,将then
的回调函数注册到微任务队列 - 执行
console.log('task5')
,输出task5
- 至此,第一轮的同步任务执行完毕
- 执行
微任务队列
- 执行
then
的回调函数,输出task6
- 微任务队列清空
- 执行
图示:
第二轮事件循环(宏任务2:setTimeout
注册的回调)
同步任务
- 执行
setTimeout
注册的回调,创建了一个Promise
- 执行
Promise
执行器函数中的console.log('task2')
,输出task2
- 执行
resolve
方法,将第一个then
的回调函数注册到微任务队列 - 至此,同步任务执行完毕
- 执行
微任务队列
- 执行第一个
then
的回调函数,输出task4
then
的链式调用,注册第二个then
的回调函数- 执行 第二个
then
的回调函数,输出task7
- 微任务队列清空
- 执行第一个
图示:
最终输出顺序是:task1 task3 task5 task6 task2 task4 task7
当面对一段包含同步、异步的代码段时,能够清楚明白其运行机制,知道输出顺序,即可算掌握
总结
文章开篇我们围绕同步任务和异步任务做了介绍:
- 同步任务按顺序,在栈中顺序执行
- 异步任务的回调函数注册到任务队列
引出了任务队列的存在后,讲解了任务队列细分为微任务和宏任务,微任务队列的优先级最高
最后是本文核心,理解事件循环,一句话来概括就是:在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行
这里要注意,结合实践代码来分析才能真正理解掌握
参考资料
JavaScript 内功系列
解析 JavaScript 核心技术,提升编程内功
本文已收录至《JavaScript 内功系列》,全文地址:我的 GitHub 博客 | 掘金专栏
对你有帮助的话,欢迎 Star
交流讨论
对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。