简单聊一聊浏览器下的 eventloop, 宏任务与微任务
函数调用栈
函数都是有调用栈的, 大多数程序可以看成函数调用函数不断调用函数, 比如下面这张图:
可以看到, 首先有一个 main(可以看做是入口), 然后依次是fn3()
调用fn2()
, 再调用fn1()
, 最后调用console.log()
. 一般出现错误的时候, 就会出现类似栈调用情况, 如下:
event loop
首先推荐观看这个视频What the heck is the event loop anyway?[1], 以及作者写了一个 event loop 可视化工具: Loupe[2], 不过目前这个工具支持比较有限, 不支持查看 callback queue 里面具体的 macrotask 和 microtask.
这里还是总结一下基本的 event loop 流程, 首先看这张图:
•首先 JavaScript 是一门单线程语言, 只有一个主线程, 你可以看成 main, 该线程负责调用函数堆栈, 也就是前面所说的Call Stack•其中有一些 API 浏览器是不提供, 所以无法处理, 主线程会把这些 API, 放到另外一个地方(绿色部分)处理, 这些 API 叫作 Web API(也可以称之为调度者), 如上图中的 setTimeout
, setTimeInterval
, XMLHttpRequest
等等•这里注意, 在处理这些 Web API 的时候, js 引擎的主线程还是在不断工作的(如果有任务的话), 可以认为两边是互相工作, 互不干扰•回到 WebAPI 部分, 假设放到 Web API 部分的这段代码是 setTimeout((_) => console.log(1), 1000)
, 这里绿色部分所做的是:•等 1000ms•将(_) => console.log(1)
这个回调放到 Event Queue(事件队列, 蓝色部分) 里面, 这里注意: 并非是把setTimeout()
整个函数放入到事件队列里面, 仅放入其中的回调函数部分•也就是说, 调度者在上下文代码中还是同步立即执行, 只不过其随后会将自己里面的回调函数放入任务队列中等待执行.•最后对于 Event Queue 部分, 这里会堆积很多 Web API 移动下来的回调函数. 当 js 引擎的 call stack 为空的时候, event queue 会把最先进入到 queue 里面的回调函数推入 call stack 里面执行
给一张图动态描述上述的过程:
几个注意点:
•web api 执行 (setTimeout, 请求等), 一旦时间到, 立马就将里面的回调函数放入到 event queue 任务队列中•一旦主线程空了, 那么不管 web api 里面是否还有代码在运行, 都会开始执行任务队列里面的任务
所以整个执行过程是相当连贯协调, 大家分工合作, 每个函数都有自己的职责和暂时归属地, 属于哪里以及什么时候被执行都是井井有条. 避免了同步情况下, 一个无关紧要的任务被卡死, 其余任务无法被执行的现象
异步任务和同步任务
•同步任务: JS 引擎主线程里面立即被推入 call stack 且可以被执行的函数•异步任务: 在 event queue 里面的回调函数, 也就是独立于主线程里面的任务
举一个例子:
下面的这个片段, synCb
这个回调函数是不会被推入到 event queue 里面执行的, 会被浏览器当做同步任务执行, 所以打印的结果就是按照从上到下面的顺序, 依次进入堆栈输出的
console.log('start')
const syncFun = function(synCb) {
const v = 100
synCb(v)
}
syncFun(v => console.log(v))
console.log('end')
// start
// 100
// end
假如我们加入异步任务, 如下, 那么这里的asynCb
是会作为异步任务放入 event queue 里面等主线程清空以后(也就是两个 console.log
都执行完毕), 才会被推入 call stack 被调用
console.log('start')
setTimeout(function asynCb() {
console.log(100)
}, 0)
console.log('end')
// start
// end
// 100
总的来说在这种情况下, js 形成了两个队列: 同步任务队列和异步任务队列, 先执行同步任务队列里面的任务, 所有同步任务执行完毕以后, 再执行异步任务队列里面的任务.
异步回调
普通函数回调
例如setTimeout(callback, timer)
, 里面的 callback 是异步任务, 最后是会被放入到 event queue 里面的, 这里要注意一个setTimeout
的点: 即里面的时间并非真正意义上的执行时间, 考虑如下代码:
setTimeout(() => {
console.log(8)
}, 5000)
setTimeout(() => {
console.log(9)
}, 5000)
setTimeout(() => {
console.log(10)
}, 5000)
// 8
// 9
// 10
上述代码最后会依次打印 8, 9, 10. 这是由于根据执行顺序会依次放入到 event queue 里面, 也就是说, 打印 8 的回调会被先放入到异步任务队列里面, 然后是 9, 10.
所以这里的时间并非指的是这个函数的执行时间, 而指代的是, 异步回调函数几秒后会被放入到任务队列
Promise 和 async/await
Promise
promise
中, .then()
里面的为异步回调
假设有如下代码:
console.log('start')
new Promise((resolve, reject) => {
resolve(1)
console.log('middle')
}).then(v => console.log(v))
console.log('end')
// start
// middle
// end
// 1
上面代码中, v => console.log(v)
是异步回调, 注意, new Promise
在实例化的过程中所执行的代码都是同步进行的, 所以console.log('middle')
会同步执行
async/await
async/await
本质上还是基于 Promise
的一些封装, async
函数在await
之前的代码都是同步执行的,可以理解为await
之前的代码属于new Promise
时传入的代码,await
之后的所有代码都是在Promise.then
中的回调
console.log('start')
async function main() {
console.log('test')
const v = await Promise.resolve(1)
console.log(v)
}
main()
console.log('end')
// start
// test
// end
// 1
写法总结
使用request[3], 写一个getUser
函数, 有基本以下写法:
回调写法:
const getUser = function(callback) {
request('http://www.example.com', function(err, res, body){
callback(body)
})
}
getUser(v => console.log(v))
Promise 写法:
const getUser = function() {
return new Promise((resolve, reject) => {
request('http://www.example.com', function(err, res, body){
if (err) {
return reject(err)
}
resolve(body)
})
})
}
getUser()
.then(v => console.log(v))
.catch(err => console.error(err))
async/await 写法:
const getUser = function() {
return new Promise((resolve, reject) => {
request('http://www.example.com', function(err, res, body){
if (err) {
return reject(err)
}
resolve(body)
})
})
}
(async function() {
try {
const v = await getUser()
console.log(v)
} catch(e) {
console.error(e)
}
})()
宏任务与微任务
上面提到的 event queue 里的异步任务还可以细分为: macrotask(宏任务), microtask(微任务). 这里注意, 现在标准称呼可以认为是 tasks 和 jobs, 但本文还是以宏任务和微任务来代指
宏任务:
•script(整体代码, 上下文)•setTimeout
, setInterval
•I/O•UI rendering
微任务:
•Promise
•process.nextTick
当然上面的任务其实均指的是其中的回调函数, 而上面的 api 前面也提过, 充当调度者的作用
有几个概念需要注意一下:
•promise.then(callback)
里面的callback
是一个微任务, 且会被推入当前的微任务队列, 当且仅当该promise
状态变更为resolved
或者rejected
, 否则不被推入任务队列•setTimeout(callback, t)
的callback
是一个宏任务, 会被推入当前的宏任务队列中, 即使t
为 0•整个在 script 中的代码也是一个宏任务
模型
关于宏任务和微任务的模型可以看成如下图的形式:
有几个重要的概念:
•始终只有一条宏任务队列•可以有多条微任务队列, 但是每一个宏任务后面仅跟随一条微任务队列, 且只对当前的宏任务"有效"•每一次 loop(循环), 都会去执行宏任务里面最前面的一个宏任务, 然后检查是否有微任务队列, 依次执行微任务队列里面的微任务. 执行完毕后开始下一次循环, 和之前一样从宏任务队列里面选取最新的宏任务执行, 不断循环•在执行微任务/宏任务的过程中, 如果发现微任务, 那么添加到当前的微任务队列中等待稍后被执行, 而如果发现宏任务, 该宏任务被添加到宏任务队列中等待下一次 loop 被执行. 也就是说: 发现微任务是可以在当前 loop 下被执行的, 而发现的宏任务只能等到下一次循环的时候被执行•每一次 loop 都包含: 一个(且只有一个)宏任务被执行, 以及对应的微任务队列, 执行完毕后开启下一个 loop
总结:
1.运行宏任务队列最先进来的宏任务, 然后移除他(一般第一个宏任务是 script)2.依次运行微任务队列中的微任务, 移除他们3.开始下一次 loop(返回过程 1)
异步任务模型伪代码可以模拟成如下:
// 第一次 loop
[
['宏任务 1', '微任务 1.1', ' 微任务 1.2'],
['宏任务 2'],
['宏任务 3'],
]
// 第二次 loop
[
// ['宏任务 1', '微任务 1.1', ' 微任务 1.2'], 执行完毕被清空
['宏任务 2', '微任务 2.1'],
['宏任务 3'],
]
// 第三次 loop
[
// ['宏任务 1', '微任务 1.1', ' 微任务 1.2'], 执行完毕被清空
// ['宏任务 2'], 执行完毕被清空
['宏任务 3'],
]
示例
示例 1:
console.log('start')
setTimeout(function() {
console.log('timeout')
}, 0)
new Promise(function(resolve) {
console.log('promise')
resolve()
}).then(function() {
console.log('promise resolved')
})
console.log('end')
// start
// promise
// end
// promise resolved
// timeout
分析:
1.建立执行上下文,进入执行栈开始执行代码,打印 start2.往下执行,遇到setTimeout
,将回调函数放入宏任务队列,等待执行3.继续往下,有个new Promise
,其回调函数并不会被放入其他任务队列,因此会同步地执行,打印promise
,但是当resolve
后,.then
会把其内部的回调函数放入微任务队列4.执行到了最底部的代码,打印出 end, 此时 call stack 被清空, 可以执行异步任务5.开始第一次 event loop:1.由于整个 script 算一个宏任务, 因此该宏任务已经被执行完毕2.检查微任务队列, 发现其中有 3 放入的微任务, 执行打印出 promise resolved,第一次循环结束6.开始第二次 loop:1.从宏任务开始,检查宏任务队列是否有可执行代码,发现有 2 中放入的一个,打印timeout2.检查微任务队列, 微任务, 第二次循环结束
示例 2:
console.log('start')
setTimeout(function () {
console.log('event loop2, macrotask')
new Promise(function (resolve) {
console.log('event loop2, macrotask continue')
resolve()
}).then(function () {
console.log('event loop2, microtask1')
})
}, 0)
new Promise(function (resolve) {
console.log('middle')
resolve()
}).then(function () {
console.log('event loop1, microtask1')
setTimeout(function () {
console.log('event loop3, macrotask')
})
})
console.log('end')
// start
// middle
// end
// event loop1, microtask1
// event loop2, macrotask
// event loop2, macrotask continue
// event loop2, microtask1
// event loop3, macrotask
分析:
1.打印 start, 发现 setTimeout
, 回调放入宏任务队列中2.发现在初始化 promise 实例, 初始化过程中打印 middle, 初始化完毕后 promise 状态变为 resolve3.resolve 后因为后面的.then()
将回调放当前入微任务队列中4.打印 end, script 完成5.第一次 event loop:1.由于 script 完成, 相当于完成了宏任务2.检查微任务队列, 发现有一个在第 3 步放入的微任务, 执行打印 event loop1, microtask1, 继续执行, 发现有setTimeout
, 将其中回调放入宏任务队列中6.第二次 event loop:1.检查宏任务队列, 发现排在最前面的是第 1 步放入的宏任务, 执行打印 event loop2, macrotask2.继续执行, 发现在初始化 promise 实例, 打印 event loop2, macrotask continue3.promise 在状态被 resolve 之后回调放入当前微任务队列中4.检查微任务队列, 发现有一个在第 6.3 中的微任务, 执行打印 event loop2, microtask17.第三次 event loop:1.发现在第 5.2 步中放入的最后一个宏任务, 执行并打印 event loop3, macrotask
一个相关问题
前两天在知乎看到的一个问题: JavaScript的DOM事件回调不是宏任务吗,为什么在本次微任务队列触发[4]
console.log('本轮任务');
new Promise((resolve, reject) => {
resolve(3)
}).then(() => {
console.log('本轮微任务');
})
document.getElementById('div').addEventListener('click', () => { console.log('click'); })
document.getElementById('div').click()
// 本轮任务
// click
// 本轮微任务
这里注意: click()
和dispatchEvent()
等人工合成事件是同步触发的, 其回调并非会被放入宏任务队列中, 而是直接作为同步任务执行, 具体答案可以参考这个回答[5] 和补充[6]
参考
•https://zhuanlan.zhihu.com/p/75572565•https://juejin.im/post/5b73d7a6518825610072b42b•https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5•https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context•https://jakearchibald.com/2015/tasks-microtas•https://www.zhihu.com/question/362096226/answer/944729236
引用链接
[1]
What the heck is the event loop anyway?: https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=1219s[2]
Loupe: http://latentflip.com/loupe[3]
request: https://github.com/request/request[4]
JavaScript的DOM事件回调不是宏任务吗,为什么在本次微任务队列触发: https://www.zhihu.com/question/362096226[5]
回答: https://www.zhihu.com/question/362096226/answer/944729236[6]
补充: https://www.zhihu.com/question/362096226/answer/999973223