setTimeout/setInterval/requestAnimationFrame/requestIdleCallback/MessageChannel

974 阅读3分钟

执行顺序:微任务----->dom渲染---->宏任务

测试:利用alert弹窗会阻碍dom渲染代码执行,来验证微任务与宏任务的执行是在dom渲染前或后

<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
 
<body>
  <div id="container"></div>
  <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
  <script>
    /*********原生**********/
    const container = document.querySelector('#container');
    container.innerHTML = 111;
 
    Promise.resolve().then(() => {
      console.log("Promise", container.childNodes);
      alert("Promise.resolve()元素渲染了么", container.childNodes);
    })
 
    setTimeout(() => {
      console.log("setTimeout", container.childNodes);
      alert("setTimeout元素渲染了么", container.childNodes);
    });
    /****************/
    /*******jquery*********/
    // const container = $('#container');
    // container.append("<p>123456</p>");
 
    // Promise.resolve().then(() => {
    //   console.log("Promise", container.children.length);
    //   alert("Promise.resolve()元素渲染了么", container.children.length);
    // })
 
    // setTimeout(() => {
    //   console.log("setTimeout", container.children.length);
    //   alert("setTimeout元素渲染了么", container.children.length);
    // });
      /****************/
  </script>
</body>
 
</html>

setTimeout/setInterval

它是宏任务。

setTimeoutsetInterval 是使用定时器来触发回调函数的,而定时器并无法保证能够准确无误的执行,有许多因素会影响它的运行时机,比如说:当有同步代码执行时,会先等同步代码执行完毕,异步队列中没有其他任务,才会轮到自己执行

 然而按照HTML 规范,嵌套深度超过 5 级的定时器,会被限制在 4ms。

setImmediate

它是宏任务。

function func(...data) {
  console.log(data)
}
setImmediate(func, 111,222)

setImmediate参数设置:第一个参数是需要执行的方法,第二个参数到第n个参数是传入方法中的参数。

setImmediate表示立即执行,回调函数会被放置到事件循环的check阶段。 在应用中如果大量的计算型任务,它是不适合放在主线程中执行的,因为计算任务会阻塞主线程,主线程一旦被阻塞,其他任务就需要等待,所以这种类型的任务最好交给C++维护线程去执行。 可以通过setImmediate方法将任务放入事件循环中的check阶段,因为代码在这一个阶段执行不会阻塞主线程,也不会阻塞事件循环。

非标准: 该特性是非标准的,请尽量不要在浏览器生产环境中使用它!node环境中可以使用。

requestAnimationFrame

requestID = window.requestAnimationFrame(callback)

requestAnimationFrame的运行机制:在每次渲染前执行,每一帧都会执行。(chrome和safari实现有差异)

举例:

<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
    myDiv.style.width = '0';
    cancelAnimationFrame(timer);
    timer = requestAnimationFrame(function fn(){
        if(parseInt(myDiv.style.width) < 500){
            myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
            myDiv.innerHTML =     parseInt(myDiv.style.width)/5 + '%';
            timer = requestAnimationFrame(fn);
        }else{
            cancelAnimationFrame(timer);
        }    
    });
}
</script>

执行顺序测试:

requestAnimationFrame(()=>{
    console.log(111);
    setTimeout(() => {
        console.log(222);
    });
    Promise.resolve().then(() => {
        console.log(333);
    });
})

requestAnimationFrame(() => {
    console.log(444);
    Promise.resolve().then(() => {
        console.log(555);
    });
})


requestIdleCallback

var handle = window.requestIdleCallback(callback[, options])

RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。故RequestIdleCallback 定位处理的是: 不重要且不紧急的任务

和requestAnimationFrame不同,在每一帧有空余时间时(正常帧任务没有超过16ms),执行requestIdCallback里面注册的任务。

因为requestIdCallback发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘。

DOM 操作建议在 rAF 中进行。同时,操作 DOM 所需要的耗时是不确定的,因为会导致重新计算布局和视图的绘制,所以这类操作不具备可预测性。  

Promise 也不建议在这里面进行,因为 Promise 的回调属性 Event loop 中优先级较高的一种微任务,会在 requestIdleCallback 结束时立即执行,不管此时是否还有富余的时间,这样有很大可能会让一帧超过 16 ms。 

举例:

type Deadline = {
  timeRemaining: () => number // 当前剩余的可用时间。即该帧剩余时间。
  didTimeout: boolean // 是否超时。
}
 
function work(deadline:Deadline) { // deadline 上面有一个 timeRemaining() 方法,能够获取当前浏览器的剩余空闲时间,
单位 ms;有一个属性 didTimeout,表示是否超时
  console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
  if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
     // 走到这里,说明时间有余,我们就可以在这里写自己的代码逻辑
  }
  // 走到这里,说明时间不够了,就让出控制权给主线程,下次空闲时继续调用
  requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // 这边可以传一个回调函数(必传)和参数(目前就只有超时这一个参数)
  • requestAnimationFrame和requestIdleCallback是和宏任务性质一样的任务,只是他们的执行时机不同而已。也有人说它们既不是宏任务也不是微任务,其实我们不必纠结这个,我们所要做的就是知道他们的执行时机就好。
  • 浏览器在每一轮Event Loop事件循环中不一定会去重新渲染屏幕,会根据浏览器刷新率以及页面性能或是否后台运行等因素判断的,浏览器的每一帧是比较固定的,会尽量保持60Hz的刷新率运行,每一帧中间可能会进行多轮事件循环。
  • requestAnimationFrame回调的执行与task和microtask无关,而是与浏览器是否渲染相关联的。它是在浏览器渲染前,在微任务执行后执行。
  • requestIdleCallback是在浏览器渲染后有空闲时间时执行,如果requestIdleCallback设置了第二个参数timeout,则会在超时后的下一帧强制执行。

执行顺序测试:

requestIdleCallback(() => {
    console.log(111);
    setTimeout(() => {
        console.log(222);
    })
    Promise.resolve().then(() => {
        console.log(333);
    })
})

requestIdleCallback(() => {
    console.log(444);
    Promise.resolve().then(() => {
        console.log(555);
    })
})

MessageChannel

它是宏任务。

const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
  console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};
port2.onmessage = function (event) {
  console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
  port2.postMessage('pong');
};
port1.postMessage('ping');

addEventListener的写法也可以。

const { port1, port2 } = new MessageChannel();
port1.addEventListener('message', function (event) {
  console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
});
port1.start();
port2.addEventListener('message', function (event) {
  console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
  port2.postMessage('pong');
});
port2.start();
port1.postMessage('ping');

MessageChannel允许我们在不同的浏览上下文,比如window.open()打开的窗口或者iframe等之间建立通信管道,并通过两端的端口(port1port2)发送消息。MessageChannel以DOM Event的形式发送消息,所以它属于异步的宏任务。

我们把port1和port2统一叫做MessagePort。 ​

MessagePort还有两个方法:closeonmessageerror。 ​

close方法能断开MessagePort的连接,之后两个断开之间将无法通信。建议通信结束后主动调用close方法以便资源回收。

消息不能反序列化时,会出现错误,这时可以用onmessageerror方法捕获。

参考:

关于setTimout和闪屏?

为什么 setTimeout 有最小时延 4ms ?

字节面试官问粉丝,如何实现准时的SetTimeout

如何实现比 setTimeout 快 80 倍的定时器?

setTimeout和requestAnimationFrame

如何实现一个零延迟的定时器?

setTimeout(〒︿〒) 请原谅我一直以来对你的忽视

定时器不准时☞带你揭秘setTimeout和setInterval

深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame

异步分片计算在腾讯文档的实践

2022年了,还不懂requestIdleCallback么?

HTML5 中的requestAnimationFrame、requestIdCallback及二者的区别

requestAnimationFrame和requestIdleCallback是宏任务还是微任务

requestAnimationFrame回调时机

浅谈MessageChannel

setImmediate方法

setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop

Effective前端6:避免页面卡顿

宏任务、微任务、dom渲染执行顺序你是否搞清了