其实JS中的事件循环还有很多细节的东西,setImmediate、nexttick、Promise A+规范等等,有深挖的价值,这一篇旨在介绍事件循环的概念以及简单地应用。
1 什么是事件循环
JavaScript是单线程语言,意味着同一时间只能做一件事情,但不等同于阻塞,而非阻塞单线程的一个实现方法就是事件循环模型。
在JavaScript中我们把所有代码分为异步代码和同步代码:
同步任务:直接在主线程执行。
异步任务:注册异步任务,当满足条件(如定时器到时,Promise被resolve等等)时推入异步任务队列。
在主线程代码执行完毕之后进行如下操作:
1、查看异步任务队列中是否存在未执行完毕的任务,如果有,则取出一个推入主线程执行
2、待主线程将该任务执行完毕后,继续查看异步任务队列.....(不断循环直到清空任务队列)。
2 宏任务微任务
按照这个思路来理解下面的代码。
console.log('start')
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise')
})
}, 0);
setTimeout(() => {
console.log('timer2')
}, 0);
console.log('end')
- 开始执行,异步队列为 [ ]
- console.log("start"),同步代码直接执行,输出 start
- setTimeout(),记timer1,异步任务,放入异步任务队列,此时异步队列 [ timer1 ]
- setTimeout(),记timer2,异步任务,同样放入队列,此时异步队列 [ timer1, timer2 ]
- console.log("end"),同步代码直接执行,输出 end
- ---------------------------------------------------------------------------------------------------------------------
- 执行完毕,查找队列,取出 timer1,异步队列 [ timer2 ]
- 执行 timer1 内部代码,输出 tiemr1 。then(),异步任务,加入队列,异步队列 [ timer2, then ]
- 执行完毕,查找队列,取出 timer2,异步队列 [ then ]
- 执行 timer2 内部代码,输出 timer2
- 执行完毕,查找队列,取出 then,执行,输出 promise
- 队列清空,执行完毕。
分析后,输出结果为 start -> end -> timer1 -> timer2 -> promise
但实际上我们的输出结果为 start -> end -> timer1 -> promise -> timer2
原因在于异步任务根据时间粒度的大小被划分为了宏任务和微任务。简单概括为 “在执行下一个宏任务之前,清空微任务队列”。
常见的宏任务:
- script(整体代码)
- setTimeout/setInterval
- postmessage
- messagechannel
- I/O(NodeJs)
- setImmediate(NodeJs)
- UI交互
- ...
常见的微任务:
- Promise.then
- MutationObserver
- process.nextTick(NodeJs)
- ...
当我们把异步任务分为宏任务和微任务后,代码的执行流程如下图所示:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完毕,查看微任务队列将队列中所有微任务依次执行完毕
- 执行下一个宏任务