我不该动你的,Event Loops
写在前面的话
本意是想好好研究下 Event Loops, 看了几篇博客后,才意识到作为前端打字员的我有多无知,这坑忒深了。
macrotask?,microtask?,MutationObserver? 这些都是啥?规范还没写清楚?不同浏览器运行的还未必一样?
但为了使自己养成经常总结的习惯,还是写点什么吧。
故事的开始
<body>
<script>
const fib = n => {
if (n <= 1) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
console.now = (text) => {
console.log(`${text} ${new Date().toLocaleString()}`)
}
setTimeout(function () {
console.now(0)
document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
console.now(1)
fib(45)
console.now(2)
}, 1000)
</script>
</body>
计算 fib(45)
是一个相当耗时的工作,在我的chrome里约需要15s左右。
问,页面什么时候会变成红色?在执行 console.now(1)
之前就变成红色了吗?
可以看到即使在 console.now(1)
执行之后,页面仍旧没有变红。
关于这个现象,可以有两种解释:
-
document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
被当作一个异步事件,作为一个 task,被添加到 event loops - 渲染引擎要等到 JS 引擎空闲时才开始工作
到底是哪一种?所以将上述代码修改下
<script>
const fib = n => {
if (n <= 1) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
console.now = (text) => {
console.log(`${text} ${new Date().toLocaleString()}`)
}
setTimeout(function () {
console.now(0)
document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
console.now(1)
fib(45)
console.now(2)
}, 1000)
setTimeout(function () {
console.now(3)
fib(45)
console.now(4)
}, 1000)
</script>
又增加了一个 setTimeout
。这样的话,如果是第一种解释,应该在 console.now(3)
运行之前,页面就变成了红色;否则就应该采取第二种解释。
运行结果如下,
可以看到在 console.now(3)
之后,页面依旧没有变色,看来就是渲染引擎要等到JS引擎完全空闲时才工作。
事情就这样结束了吗
没有,直到我看到文档
An event loop must continually run through the following steps for as long as it exists:
- Let oldestTask be the oldest task on one of the event loop's task queues, if any, ignoring, in the case of a browsing context event loop, tasks whose associated
Document
s are not fully active. The user agent may pick any task queue. If there is no task to select, then jump to the microtasks step below.- Set the event loop's currently running task to oldestTask.
- Run oldestTask.
- Set the event loop's currently running task back to null.
- Remove oldestTask from its task queue.
- Microtasks: Perform a microtask checkpoint.
- Update the rendering: If this event loop is a browsing context event loop (as opposed to a worker event loop), then run the following substeps.
…...
这段话第7点的意思,怎么理解起来像是每执行一次 Event Loops 的 task,最后都会更新视图。
后来看到从event loop规范探究javaScript异步及浏览器更新渲染时机中
渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。
会不会两次 setTimeout 被合并了?
<script>
const fib = n => {
if (n <= 1) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
console.now = (text) => {
console.log(`${text} ${new Date().toLocaleString()}`)
}
setTimeout(function setTimeout0() {
console.now(0)
document.body.style.backgroundColor = 'red'
console.now(1)
fib(45)
console.now(2)
}, 1000)
setTimeout(function setTimeout1() {
console.now(3)
fib(45)
console.now(4)
}, 1001)
</script>
这样调整之后,在运行 console.now(3)
之前,页面的颜色就变了
这样看来,就是在每一次task之后就可能会更新视图,而不是等到JS引擎空闲
在执行完setTimeout0
后,Event Loops 中实际上仍有 setTimeout1
待执行,但是浏览器先渲染了视图,再执行了setTimeout
,这就推翻了之前渲染引擎要等到 JS 引擎空闲(Event Loops为空)时才开始工作。
同时我怀疑,之前代码
setTimeout(function () {
console.now(0)
document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
console.now(1)
fib(45)
console.now(2)
}, 1000)
setTimeout(function () {
console.now(3)
fib(45)
console.now(4)
}, 1000)
会不会被优化成
setTimeout(function () {
console.now(0)
document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
console.now(1)
fib(45)
console.now(2)
console.now(3)
fib(45)
console.now(4)
}, 1000)
坑深,今天先到这,休息下了
参考资料
- https://github.com/aooy/blog/...
- https://www.404forest.com/201...
- http://javascript.ruanyifeng....
- https://www.html5rocks.com/zh...
- https://github.com/fredshare/...
- http://lynnelv.github.io/js-e...
- https://www.w3.org/TR/html5/w...
- https://jakearchibald.com/201...
- https://zhuanlan.zhihu.com/p/...
- https://developers.google.com...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。