在浏览器的一次render中,同步任务执行完毕后,首先会检查宏任务中有没有优先级很高的操作(比如ui事件),如果有会优先执行
,没有则执行微任务,如果在清空微任务队列中又有新的微任务被添加进来,继续执行微任务,直至清空微任务队列,然后再去执行宏任务(如此循坏)。
事件循环中可能会有一个或多个任务队列,这些队列分别为了处理:
- 鼠标和键盘事件
- 其他的一些 Task
浏览器会在保持任务顺序的前提下,可能分配四分之三的优先权给鼠标和键盘事件
,保证用户的输入得到最高优先级的响应
,而剩下的优先级交给其他 Task
,并且保证不会“饿死”它们。
流程
-
从任务队列中取出一个宏任务并执行。
-
检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
-
进入更新渲染阶段,判断是否需要渲染,这里有一个
rendering opportunity
的概念,也就是说不一定每一轮 event loop 都会对应一次浏览器渲染
,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)
注意:浏览器渲染与dom更新并不相同, dom更新是在当前宏任务的所有微任务执行后,下个宏任务前 进行
```js
// 执行下面代码,看到只有1次浏览器刷新渲染 ,
// 因为两个宏任务的dom更新间隔时间特短,在1次屏幕时间(通常16.7ms)内,所以只看到了1次刷新
setTimeout(() => {
document.body.style.background = "red"
setTimeout(() => {
document.body.style.background = "blue"
})
})
```
类型
任务类型 | 事件类型 |
---|---|
宏任务 | setTimeOut 、 setInterval 、 setImmediate 、 I/O 、 各种callback 、 UI渲染 、messageChannel 、 ajax网络请求 、 fs.readFile 等 |
微任务 | process.nextTick 、Promise 、MutationObserver 、async(实质上也是promise) |
微任务
MutationObserver
MutationObserver 接口提供了监视对DOM树所做的更改能力。它被设计为旧的MutationEvents功能的替代品。
MutationObserver
是一个构造器,他能够在指定的DOM
发生变化的时候被调用。
process.nextTick
process.NextTick是nodeJS中的概念,在浏览器中并不能够使用哦,node官网传送门👉
准备另开一篇文章:去学习node_eventLoop,传送门👉
console.log('start');
process.nextTick(() => {
console.log('nextTick callback');
});
console.log('scheduled');
// start
// scheduled
// nextTick callback
注意:每次事件轮询后,在额外的I/O
执行前,next tick
队列都会优先执行。 递归调用nextTick callbacks
会阻塞任何I/O操作,就像一个while(true);
循环一样。
宏任务
-
ui交互事件
是优先级高的宏任务
(click,input,mouseenter,keyup ,scroll等)
比如scroll
,并不是到了这一步才去执行滚动和缩放,那岂不是要延迟很多?浏览器当然会立刻帮你滚动视图,根据**CSSOM 规范**所讲,浏览器会保存一个pending scroll event targets
,等到事件循环中的scroll
这一步,去派发一个事件到对应的目标上,驱动它去执行监听的回调函数而已。resize
等也是同理。就是说浏览器让该类交互事件,ui层面先完成,其余事件处理等遵循时间循环中宏任务的执行原理
。 -
postMessage (
优先级不高的宏任务
)基本用法
let ch = new MessageChannel() let p1 = ch.port1; let p2 = ch.port2; p1.postMessage("你好我是 p1"); // port2 receive 你好我是 p1 p2.postMessage("这样啊,我是p2,吃了吗?"); // port1 receive 这样啊,我是p2,吃了吗?
MDN 示例
// 使用MessageChannel构造函数实例化了一个channel对象 var channel = new MessageChannel(); var para = document.querySelector('p'); // 获取到iframe对象 var ifr = document.querySelector('iframe'); var otherWindow = ifr.contentWindow; // 当iframe加载完毕 ifr.addEventListener("load", iframeLoaded, false); // 我们使用MessagePort.postMessage方法把一条消息和MessageChannel.port2传递给iframe function iframeLoaded() { otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]); } // channel.port1.onmessage = handleMessage; function handleMessage(e) { para.innerHTML = e.data; }
-
webworker
后来笔者在工作使用到了service-worker帮助进行排序计算,所以补充一下,主页面和worker之间的通信也是是用了 MessageChannel 机制进行实现
// page.js let worker = new Worker('./counting.js'); worker.postMessage({id:666}) worker.on('message',result=>{ rending(result); // 渲染结果 }) // counting.js self.on('message',message=>{ let result = countintMethod(message.id); self.postMessage(result); })
这样至少有一个好处就是能够不阻塞浏览器UI的渲染,让另一个进程去帮助我们进行计算,然后异步渲染。
-
setTimeout、setInterval、setImmediate(
优先级不高的宏任务
)几个定时器属于宏任务这个不多说。简单提一下
setImmediate
,它相当于setTimeout(fn,0)
,一般我们会将他用于把某个任务提取到异步的形式执行,而不阻塞当前任务。
requestAnimationFrame - 不是宏任务的任务 (会在下1次渲染前执行
)
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,
该回调函数会在浏览器下一次重绘之前执行
--- MDN ,解决了使用setTimeout等制作动画,时间上可能不靠谱的问题
严格意义上来说,raf并不是一个宏任务,因为
- 执行时机和宏任务完全不一致。
- raf任务队列被执行的时候,会将其此刻队列中所有的任务都执行完。
但是查阅了相关规范之后,我们可以知道一个eventLoop的完整过程是包含这浏览器的渲染过程的
,再根据上面raf
的定义可以知道,ref的执行会在一个eventLoop
中的微任务结束后,下一个eventloop
开始前去执行。
console.log(1)
setTimeout(function() {
console.log(2)
}, 0) // 注意这的数值 改为10试下
const p = new Promise((resolve, reject) => {
resolve(3)
})
p.then(data => {
console.log(data)
})
requestAnimationFrame(() => {
console.log(4)
})
console.log(5)
解释:requestAnimationFrame是渲染前执行
,setTimeout中的间隔数值不同导致,requestAnimationFrame的出现顺序不同
。因为间隔设置为0-1的时候,由于间隔时间短,执行到宏任务setTimeout,并没有导致渲染
,浏览器是在一次渲染中,。但是大于等于1,导致增加1次渲染, 所以requestAnimationFrame显示的顺序不同了
判断宏任务与ui线程执行顺序
document.getElementsByTagName('h1')[0].addEventListener('click',function(){
setTimeout(() => {
while(true){
console.log(1)
}
});
this.style.color="red"
})
// 执行结果:元素变红,然后进入while循环打印 1
复制代码
判断微任务与ui线程执行顺序
document.getElementsByTagName('h1')[0].addEventListener('click',function(){
Promise.resolve(2).then(res => {
while(true){
console.log(res)
}
}
this.style.color="red"
})
// 执行结果:进入while循环打印 2
.为什么vue的真实dom操作会优先选择微任务呢?
答:因为微任务在ui更新队列之前,每一个真实dom操作都会给ui更新队列中推入一个新的任务,所以在拉起ui线程的时候,只需要拉取一次清空ui线程中的任务就行,如果放在宏任务的话,每一次宏任务所生产的ui更新队列任务都会造成一次ui线程的拉起,如果有一百个宏任务dom操作就会拉起100次ui线程,这样会对性能造成极大的影响
参考文章
[1] MessageChannel
[2] MutationObserver - MDN
[3] MutationObserver的使用 - 掘金
[4] requestAnimationFrame是一个宏任务么
[5] Living Standard — Last Updated 6 October 2020
[6] 渲染