我们经常看到 setTimeout 中延迟设置为 0,但真的是 0 延迟吗?
事件循环
这涉及到 javascript 的事件循环机制
众所周知,javascript 是单线程的,但是却支持异步,和 CPU 非常相似
对照 CPU 的进程调度,可以发现任务是排队执行的,并且有不同的优先级队列
但是我们可以发现,以下输出不是按顺序的
1 | Promise.resolve().then(res=>console.log('then1')).then(res=>console.log('then4')) |
实际运行一下,得到输出是
1 | promise |
为什么会这样??
这就涉及到宏任务和微任务的概念
宏任务和微任务
上文中提到单线程,任务队列,事件循环
之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:
1 | while (queue.waitForMessage()) { |
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个
任务队列又分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 和 jobs
用操作系统说法,就是进程和线程的区别
上例可以做如图表示
因为 new Promise 中 settled 之前的函数都是同步进行的,所以先打印 promise
当这一轮主线程执行完毕的时候,变为如图情况
此时,引擎发现,虽然当前主线程为空,但微任务队列不为空,所以当前宏任务还不算结束
于是引擎将微任务队列的任务都塞进主线程,这个过程就是 开启下一次事件循环
再次执行完毕后,变为如图情况
此时微任务队列依然不为空,再次优先执行微任务队列中的任务
执行完后变为如图情况
微任务队列终于空了!主线程开始取宏任务队列中的队首任务,变为如图情况
执行完毕后,发现微任务队列还是空的,于是再取宏任务,再空,终于所有任务都执行完了!
这就是 JS 事件调度的过程,可见 setTimeout(fn,0) 并不是真正的 0 延迟执行,而是“尽可能快”
显然代码可以分为三种
- 立即执行的代码
- 微任务
- 宏任务
则事件循环的实现,大致有如下伪代码
1 | while(true){ |
文字描述如下
- 处理主线程中所有任务
- 若微任务队列为空
- 若宏任务队列不为空,则取宏任务队列的队首到主线程中,并在开启下一个事件循环前,进行页面渲染
- 若微任务队列不为空,则将微任务队列中所有任务加入主线程中
那么哪些是微任务,哪些是宏任务呢?
一般有如下认定与支持
宏任务 | 浏览器 | 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)—宏任务和微任务
谢谢阅读