上一篇: 写一个模板引擎下一篇: 正则表达式

Macro task/Micro task


本文主要参照 stackoverflow 上的回答和「高性能JavaScript」

在浏览器中, UI更新(重排重绘)js任务 在同一个 任务队列 中执行, 如果当前任务很耗时, 则会阻塞整个线程, 后续所有任务包括 UI更新 将一直处于 挂起(pending) 的状态, 此时浏览器是假死的状态. 如上图所示, 所有任务(js任务或UI更新)都会追加到队列末尾, 不管任务执行多久, 在这段时间之内, 后续任务都处于 挂起 状态, 一旦队列空闲, 则立即执行下一个任务.

尝试运行以下代码

var t = Date.now();
setTimeout(_ => console.log(123), 1000);
while (Date.now() - t <= 5000) {}

上面代码中, 在第2行执行 setTimeout 会异步地在1s之后分配一个任务到任务队列, 然后马上执行耗时5s的循环, 当时间过去1s, 其他线程会添加一个 console.log(123) 的任务到js执行线程, 因为js是单线程执行且此时并不空闲, 所以必须等到5s执行完之后才会立即打印123.

macro task 译为 宏任务, micro task 译为 微任务, 整个 js 的主任务队列就是 宏任务队列, 但在不同宏任务之前会穿插着不同的 微任务队列

每次 宏任务队列中某个宏任务执行完之后会立即检查紧跟在后面的微任务队列, 若此微任务队列为空, 则立即执行UI更新任务(如果UI需要更新), 然后开始下一个宏任务执行.

什么是 一个任务 ?

在 js 执行过程中, 一个任务就是 调用栈(call stack) 从入栈到全部出栈的过程, 换句话说, 就是一段同步代码的顶层作用域从头到尾执行一遍的过程, 这个过程一下简称 js任务, 一个js任务可能被添加到 宏任务 或 微任务 的队列中, 关键在于它是一个同步的任务

除了 js任务, 还有其他的任务, 比如 UI更新, UI更新在浏览器中得到了优化, 多个任务合并到一次更新(如代码所示). 在 Nodejs 中有 I/O操作等, I/O 有单独的队列存放即将执行的任务, 主线程在空闲时会不断从这些队列中取出任务执行.

// 同一个任务中, 对 UI 进行若干次操作都会被浏览器合并为一次
for (var i = 0; i < 100; i++) {
    dom.style.left = i + 'px';
}

Macro Task - 宏任务:

Micro Task - 微任务:

值得注意的是

例子:

var logo = document.getElementById('logo');
var t = Date.now();

logo.style.border = '10px dashed #f70'

setTimeout(_ => logo.style.border = '5px solid #6cf')

Promise.resolve().then(_ => {
    while (Date.now() - t < 3000) {}
})

// 等待3秒, 页面渲染 10px dashed #f70  -> 说明 micro task 在ui更新之前
// 然后立即渲染      5px solid #6cf    -> 说明 macro task 在ui更新之后

在实际开发中, 了解 Macro task/Micro task 会避免掉入很多的坑. 如果js执行时间过长跨可能造成UI的帧率下降, 直接导致动画的不流畅, 页面假死等等问题, 所以 微信小程序Electron中将UI线程和和 service 线程直接采用异步通信, 这对页面流畅性提升有很大帮助.

常用的优化方式

上一篇: 写一个模板引擎下一篇: 正则表达式