事件循环

我们经常看到 setTimeout 中延迟设置为 0,但真的是 0 延迟吗?

事件循环

这涉及到 javascript 的事件循环机制

众所周知,javascript 是单线程的,但是却支持异步,和 CPU 非常相似

对照 CPU 的进程调度,可以发现任务是排队执行的,并且有不同的优先级队列

但是我们可以发现,以下输出不是按顺序的

1
2
3
4
5
6
Promise.resolve().then(res=>console.log('then1')).then(res=>console.log('then4'))
setTimeout(()=>{console.log('setTimeout1')},0)
Promise.resolve().then(res=>console.log('then2')).then(res=>
setTimeout(()=>{console.log('setTimeout2')},0))
new Promise((resolve,reject)=>{console.log('promise')})
Promise.resolve().then(res=>console.log('then3'))

实际运行一下,得到输出是

1
2
3
4
5
6
7
promise
then1
then2
then3
then4
setTimeout1
setTimeout2

为什么会这样??

这就涉及到宏任务和微任务的概念

宏任务和微任务

上文中提到单线程,任务队列,事件循环

之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

一个线程中,事件循环是唯一的,但是任务队列可以拥有多个

任务队列又分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 和 jobs

用操作系统说法,就是进程和线程的区别

上例可以做如图表示

因为 new Promise 中 settled 之前的函数都是同步进行的,所以先打印 promise

当这一轮主线程执行完毕的时候,变为如图情况

此时,引擎发现,虽然当前主线程为空,但微任务队列不为空,所以当前宏任务还不算结束

于是引擎将微任务队列的任务都塞进主线程,这个过程就是 开启下一次事件循环

再次执行完毕后,变为如图情况

此时微任务队列依然不为空,再次优先执行微任务队列中的任务

执行完后变为如图情况

微任务队列终于空了!主线程开始取宏任务队列中的队首任务,变为如图情况

执行完毕后,发现微任务队列还是空的,于是再取宏任务,再空,终于所有任务都执行完了!

这就是 JS 事件调度的过程,可见 setTimeout(fn,0) 并不是真正的 0 延迟执行,而是“尽可能快”

显然代码可以分为三种

  1. 立即执行的代码
  2. 微任务
  3. 宏任务

则事件循环的实现,大致有如下伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while(true){
while(!mainQueue.empty()) {
solve(mainQueue.front());
mainQueue.pop();
}
if(microQueue.empty()){
if(!macroQueue.empty()){
mainQueue.push(macroQueue.front());
macroQueue.pop();
render.run();
continue;
}
} else {
while(!microQueue.empty()){
mainQueue.push(microQueue.front());
microQueue.pop();
}
}
}

文字描述如下

  • 处理主线程中所有任务
  • 若微任务队列为空
    • 若宏任务队列不为空,则取宏任务队列的队首到主线程中,并在开启下一个事件循环前,进行页面渲染
  • 若微任务队列不为空,则将微任务队列中所有任务加入主线程中

那么哪些是微任务,哪些是宏任务呢?

一般有如下认定与支持

宏任务 浏览器 Node
I/O 1 1
setTimeout 1 1
setInterval 1 1
setImmediate 0 1
requestAnimationFrame 1 0
script 代码块 1 0
微任务 浏览器 Node
Promise.then/catch/finally 1 1
process.nextTick 0 1
MutationObserver 1 0

参考链接

JavaScript的事件队列(Event Queue)—宏任务和微任务

并发模型与事件循环


谢谢阅读

--It's the end.Thanks for your read.--