JS宏任务,微任务,DOM渲染,requestAnimationFrame执行顺序比较

3,485 阅读5分钟

本文小结

  1. 宏任务和微任务都是异步任务。

  2. 优先级:同步任务 > 微任务 > requestAnimationFrame > DOM渲染 > 宏任务

  3. requestAnimationFrame慢于同步任务。本次实践中,第一次执行的时间比微任务慢,比宏任务和DOM渲染快。

来自MDN的理论部分

事件循环(Event loops)

每个代理都是由事件循环驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。

任务 vs 微任务

一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。

任务队列(或许我们可以看作是宏任务队列)和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

实践部分

同步任务,宏任务和微任务的比较

console.log("start");

setTimeout(() => {
    console.log("setTimeout 0");
}, 0)

let p1 = new Promise((resolve, reject) => {
    console.log("p1 Promise executor");
    setTimeout(resolve("result1"), 0);
}).then((value) => {
    console.log("p1 Promise then:", value);
})

let p2 = Promise.resolve("result2").then((value) => {
    console.log("p2 Promise then:", value);
})

console.log("end");

输出如下:

image.png

  1. 同步任务优先顺序执行:Promise中的执行器executor是同步执行的,所以输出start,executor,end。
  2. 异步任务中,微任务的优先级高于宏任务。Promise.then是微任务,定时器setTimeout是宏任务,所以先输出then,最后输出timeout

同步,异步和DOM渲染的比较

我们先比较同步任务和DOM渲染的情况:下列代码中,创建并增加DOM节点,后进行alert输出(卡住程序的同时还能观看DOM是否渲染完毕)。

image.png

我们都知道script标签是一个宏任务,只有执行完一次宏任务(1个script标签)完毕后,才会去清空事件循环队列中其他的微任务。接着再去执行宏任务。

image.png

测试发现,DOM渲染即便在后续的script结束后,也没有执行完毕。一眼可以看出,DOM渲染肯定比同步代码慢,同时也证明DOM渲染不是微任务,至少比微任务要慢。

接着我们测试DOM渲染与宏任务和微任务的执行相对顺序:

代码如下:

<script>
    //DOM渲染
    let dom = document.createElement("div");
    dom.className = "box";
    console.log("createElement");
    document.body.appendChild(dom);
    console.log("append Child");

    //定时器
    setTimeout(() => {
        alert("setTimeout 0");
    }, 0)

    // Promise
    let p1 = new Promise((resolve, reject) => {
        console.log("p1 Promise executor");
        setTimeout(resolve("result1"), 0);
    }).then((value) => {
        console.log("p1 Promise then:", value);
    })

    let p2 = Promise.resolve("result2").then((value) => {
        console.log("p2 Promise then:", value);
    })
    // 同步alert
    console.log("end");
    alert("第一个script结束");
</script>
<script>
    alert("元素出现了吗?");
</script>

当第一个script标签的最后一个同步任务结束前,我们仅执行了所有的同步任务。

image.png

输出第二个script标签的同步任务alert后:DOM依旧未渲染,此时已经执行完微任务了。第一个script里面的宏任务setTimeout也没有执行。

image.png

后续可以看到,当宏任务执行完毕后,DOM渲染已经完成了。

image.png

小结:

  1. 执行完当前宏任务下的所有同步任务。Call Stack
  2. 微任务在dom渲染之前执行,宏任务在dom渲染之后执行。

同步,异步和requestAnimationFrame比较

这里需要强调一下,requestAnimationFrame不是定时器!不是定时器!!!只是一个用作定时器的帧函数,除非自己调用自己,否则只是在某一帧内调用一次函数罢了。

在上面的基础上,我们在最开始的代码加入requestAnimationFrame的执行任务:

function show(){
    alert("requestAnimationFrame execute");
}

requestAnimationFrame(show);
//后续代码同上
//DOM 渲染,setTimeout的宏任务,同步代码,Promise.then的微任务...

测试后可以发现,输出requestAnimation的提示时机 卡在第二个script标签结束后以及DOM渲染完成前。

由于截图观察顺序不好看,故这里直接给出输出顺序:

  1. 执行第一个script标签

同步代码直接执行,异步任务丢到任务队列(宏任务队列或者微任务队列)里

  1. 第一个script结束

一个宏任务结束,即将遍历已有的微任务队列

  1. 执行第一个script标签内微任务(Promise.then)

执行完微任务队列后执行下一个宏任务(下一个script标签,而非第一个script标签增加的定时器。)

  1. 第二个script结束

一个宏任务结束,即将遍历已有的微任务队列。由于我们第二个script没有增加微任务,故会执行宏任务。

  1. 执行requestAnimationFrame

测试多次,requestAnimationFrame的第一次执行时机总是卡在这里。

  1. DOM渲染完毕

DOM渲染比宏任务快一点。

  1. 执行定时器宏任务

接着第4步,微任务队列的结束,执行新的宏任务,由于后续没有script标签了,所以执行最早push的宏任务,即第一个script标签里setTimeout的宏任务。

后话

最后再次小结一下,本次实践的优先级:

同步任务 > 微任务 > requestAnimationFrame > DOM渲染 > 宏任务

但是面试的时候问了下面试官,他说requestAnimationFrame的执行时机的是慢于同步任务,但是和宏任务的执行时机是不确定的。

或许是JS执行它们的机制本身就不相同?一个是刷新帧的时候执行的,一个是在事件循环队列里面执行的?

上网没找到什么相关资料。还望大佬批评指正。