事件循环是啥
学js的都知道,js是单线程的,由js引擎线程单独运行,所以它的性质决定在js(除web worker,和nodejs多进程)里,不会有真正的异步出现,连并发都不是,而是以事件轮询的方式执行微任务和宏任务:
举个栗子:
setTimeout(()=>{
console.log(1)
},0)
new Promise( re =>{
console.log(2)
re(3)
}).then(val=>{
console.log(val)
})
console.log(4)
结果顺序输出 2 4 3 1
其中setTimeout是宏任务,promise是微任务,promise里的内容会立即执行,但是then方法指定的回调函数会在脚本同步任务执行完毕之后执行,而setTimeout等宏任务会等微任务和同步任务执行完毕之后最后执行。
策略是轮询,先把同步任务调入主执行栈,执行完先找微任务,有就将微任务调入执行栈执行,没有就找宏任务。所以js中的异步并不是并发,而是滞后。
下面我们来看看页面绘制与这些任务的顺序关系。
第一帧重绘与promise的顺序
我们先来看一下代码的执行顺序。
const div = document.querySelector('div')
div.addEventListener('click', function() {
console.log(1)
this.style.backgroundColor = 'black'
Promise.resolve(1).then(() => {
while (true) {
console.log('promise')
}
})
})
我们预期是先执行同步的this.style.backgroundColor = 'black'
将div设置成为黑色,然后再执行promise里面的死循环。但最终结果是控制台先输出1,然后再一直输出promise,并且div永远不会变成黑色,提前被promise里面的死循环阻塞了。也就是说,页面重绘是滞后于微任务的。
我们再进行一个试验
页面重绘与微任务、宏任务的顺序
const div = document.querySelector('div')
div.addEventListener('click', function() {
setTimeout(() => {
while (true) {
console.log('setTimeout')
}
}, 0)
this.style.backgroundColor = 'black'
})
结果是div成功变成了黑色,控制台不停地输出setTimeout,所以总地来说,页面重绘是夹在微任务与宏任务中间。
我们再来看一下requestAnimationFrame
requestAnimationFrame与页面重绘的顺序
mdn文档上说它是在每一次重绘前执行的。
const div = document.querySelector('div')
div.addEventListener('click', function() {
this.style.backgroundColor = 'black'
requestAnimationFrame(() => {
while (1) {
console.log('requestAnimationFrame')
}
})
})
结果是不变黑,也就是表明其确实是在重绘之前执行
requestAnimationFrame与promise的顺序
const div = document.querySelector('div')
div.addEventListener('click', function() {
requestAnimationFrame(() => {
while (1) {
console.log('requestAnimationFrame')
}
})
Promise.resolve(1).then(() => {
while (true) {
console.log('promise')
}
})
})
控制台一直打印promise,也就是说,滞后与promise。
所以我们就可以知道执行的优先度了
结论
const body = document.querySelector('body')
const div = document.querySelector('div')
body.addEventListener('click', function() {
div.style.display = 'block'
requestAnimationFrame(() => {
div.style.height = '200px'
})
})
上面的代码是
同步任务>promise等微任务>制作render树(display = ‘block’)>requestAnimationFrame>制作render树(height = ‘200px’(对比前一颗render的相同元素产生动画))>第一帧重绘完成>setTimeout等宏任务通过这个优先度可以去理解很多玄学的东西。
拓展
比如说下面这个为什么不会产生动画。
const body = document.querySelector('body')
const div = document.querySelector('div')
body.addEventListener('click', function() {
div.style.display = 'block'
div.style.height = '200px'
})
它的周期是
同步任务>promise等微任务>制作render树(display = ‘block’,height = ‘200px’)>第一帧重绘完成>setTimeout等宏任务
这里根本对比不了同一个元素节点的样式,因为display:none
的时候元素根本不会被画进render树,在制作(display = ‘block’,height = ‘200px’)的dom树时,只会把这节点看成新增的,新增的默认不会加动画。
通过draf可以将阻塞的dom操作放入第二帧,然后在第一帧的时候开始将gpu放入动画。