首页/文章/八股文

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

2025-03-19
21058 分钟
...

本文小结

  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");

输出如下:

 

 

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

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

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

 

 

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

 

 

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

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

代码如下:

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

    
    setTimeout(() => {
        alert("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");
    alert("第一个script结束");
</script>
<script>
    alert("元素出现了吗?");
</script>

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

 

 

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

 

 

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

 

 

小结:

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

同步,异步和requestAnimationFrame比较

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

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

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

requestAnimationFrame(show);


测试后可以发现,输出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执行它们的机制本身就不相同?一个是刷新帧的时候执行的,一个是在事件循环队列里面执行的?

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2025-03-24
OPTIONS请求条件与浏览器安全策略CORS
1\. 什么时候会发送OPTIONS请求 在 Web 开发中, 请求是一种 HTTP 请求方法,通常与 CORS(跨源资源共享,Cross Origin Resource Sharing) 机制相关。它主要用于预检(preflight)请求,以检查服务器是否允许特定的跨域请求。以下是详细说明: 1.1. 发送OPTIONS请求条件 1.1.1. 跨域请求且不是简单请求 当发起的请求是跨域的(即请求的目标域名与当前页面域名不同),并且不满...
面试面试官
八股文2025-03-24
前端项目中,强缓存和协商缓存的配置
前端缓存分为HTTP缓存和 1. HTTP缓存(本文重点) 强缓存 协商缓存 2. 浏览器缓存 比较熟悉的 cookie,localstorage sessionstorage indexDB… 或者cacheStorage 请求的缓存,如果本地有取本地的 先说总结的内容 webpack配置(主要是文件名上有个hash值,便于服务器校验文件有没有修改) 使用hash的好处就是,使版本发布之后,使之前文件的强缓存失效 nginx配置 (主...
面试面试官
八股文2025-03-24
搞定前端性能优化问题
一、首屏加载优化 首屏加载是指 用户打开页面到第一个可视内容呈现出来之间的时间 。它直接影响用户对网站的第一印象,因此在前端性能优化中具有最高的优先级。 优化首屏加载的目标是 尽可能快地把关键内容展示给用户,减少白屏时间,提升首屏体验。 常见的优化方式包括资源按需加载、预加载关键资源、使用服务端渲染或静态生成等。 1\. 资源按需加载(Lazy Load) 在前端应用中,JS、CSS、图片等资源如果全部打包在一起加载,会极大拖慢页面的加...
面试面试官
八股文2025-03-18
一文总结vue和react的区别
Vue.js 和 React.js 都是流行的 JavaScript 前端框架,用于构建用户界面。它们在设计理念、语法和使用方式等方面有一些区别,以下是一些比较详细的对比: 设计理念: Vue.js :Vue.js 是一款轻量级、渐进式的框架,它专注于视图层的渲染和状态管理。Vue 的核心思想是通过数据驱动视图的变化,提供了一套简洁的模板语法和响应式数据绑定机制,使得开发者可以更便捷地构建交互式的用户界面。 React.js :Reac...
面试面试官
八股文2025-03-10
最全的Map 和 WeakMap的区别
和 的区别及面试追问知识点 一、两者区别总结 (一)基本概念 和 均为 JavaScript 中用于存储键值对的数据结构,但在多个关键特性上有显著差异。 (二)键的类型限制 1. 键类型无严格限制,可接受基本数据类型(如字符串、数字、布尔值)以及引用数据类型(如对象、数组、函数)。 示例代码: 2. 键必须为对象,使用基本数据类型作为键会抛出错误。 示例代码: (三)垃圾回收机制 1. 对键和值采用强引用,只要 对象存在,其引用的键值对...
面试面试官
八股文2025-03-07
hash与history路由模式区别hash与history的区别
hash与history的区别 两种路由模式原因原因 对于 这类渐进式前端开发框架, 为了构建 SPA(单页面应用), 需要引入前端路由系统, 这也就是 存在的意义。 前端路由的核心,就在于 —— 改变视图的同时不会向后端发出请求。 因此有两种路由 1. : 即地址栏 URL 中的 符号(此 不是密码学里的散列运算)。 比如这个 URL: , 的值为 。它的特点在于:hash 虽然出现在 URL 中, 但不会被包括在 HTTP 请求中,...
面试面试官

评论

请登录后发表评论

去登录
加载评论中...

目录