执行顺序:微任务----->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
它是宏任务。
setTimeout 或 setInterval 是使用定时器来触发回调函数的,而定时器并无法保证能够准确无误的执行,有许多因素会影响它的运行时机,比如说:当有同步代码执行时,会先等同步代码执行完毕,异步队列中没有其他任务,才会轮到自己执行。
然而按照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等之间建立通信管道,并通过两端的端口(port1和port2)发送消息。MessageChannel以DOM Event的形式发送消息,所以它属于异步的宏任务。
我们把port1和port2统一叫做MessagePort。
MessagePort还有两个方法:close和onmessageerror。
close方法能断开MessagePort的连接,之后两个断开之间将无法通信。建议通信结束后主动调用close方法以便资源回收。
消息不能反序列化时,会出现错误,这时可以用onmessageerror方法捕获。
参考:
setTimeout和requestAnimationFrame
定时器不准时☞带你揭秘setTimeout和setInterval
深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame
2022年了,还不懂requestIdleCallback么?
HTML5 中的requestAnimationFrame、requestIdCallback及二者的区别
requestAnimationFrame和requestIdleCallback是宏任务还是微任务