有没有担心过 JS 中执行顺序的问题?如以下的代码,在页面上添加一个 el,并将它隐藏。
document.body.appendChild(el);
el.style.display = 'none';
假如 appendChild()执行得足够快的话,能否在网页上看到一闪而过的 el 呢?答案是不可以的,这与网页的 Main thread 有关。
Main Thread
网页中的main thread 是唯一的,即网页在单线程中运行,无论是 js 的运行,还是页面的渲染,还是 dom 的操作,都是在同一个 main thread 中操作的。代码的运行和页面的渲染顺序是严格定义的并且大多数情况下都是确定的。而这又要归功于 Event Loop
Event Loop
Event Loop 名称来源于它的实现原理
while (queue.waitForMessage()) {
queue.processNextMessage();
}
在 Event Loop 中有几个重要概念


-
Heap
对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。
-
Stack
栈代表了 JS 代码执行时的单线程,从栈中进行函数调用。
-
Queue
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都有一个为了处理这个消息相关联的函数。
在事件循环期间的某个时刻,运行时总是从最先进入队列的一个消息开始处理队列中的消息。正因如此,这个消息就会被移出队列,并将其作为输入参数调用与之关联的函数。为了使用这个函数,调用一个函数总是会为其创造一个新的栈帧,一如既往。函数的处理会一直进行直到执行栈再次为空;然后事件循环将会处理队列中的下一个消息(如果还有的话)。 -
Browser or Web APIs
这些是内置在网页浏览器中的函数,它们可以提供从浏览器或者电脑运行环境中获取的数据, 从而让你可以执行某些复杂的操作。这些函数不是 JS 自带的,而是建立在 JS 核心之上,使你的 JS 代码更加强大。
setTimeout 的执行顺序
setTimeout 就属于 Web APIs,它设置一个计时器,当计时器时间到了,它再执行 callback 函数。
⚠️ 注意:
- 并不是计时器时间到了,calllback 函数就立即执行,计时器的时间只是最小等待时间,过了那个等待时间,callback 函数才有机会执行。所以 callback 函数的执行可能会有延迟,甚至不执行(如果在执行栈中执行的函数是个死循环函数)。
- 即使 设置的 计时器时间为 0,也不代表 callback 函数立刻就执行,而是需要等待处理请求的最小时间, 因浏览器而异。一般为 4.7ms。
考虑 setTimeout(callback, ms)
的执行顺序:
第一想法是不是这样:
1. 等待ms毫秒
2. 调用callback函数
然后这是不对的,因为 setTimeout 是一个异步函数,它在一个跟 main thread 平行的 thread 中执行,在过了 ms 毫秒之后,再将 callback 函数以任务的形式加入到队列中,因此真正的执行顺序为:
1.在一个平行的线程中执行以下步骤:
1. 等待ms毫秒
2. 将callback函数以任务的形式加入到队列中,执行以下步骤:
1.调用callback函数
Task queues
如下图片所示,假如将电脑环境比作一个圆环形的管道,只要 cpu 在正常运行,Event Loop 就是管道中的那个小方块, 不停地转啊转。

当在 queue 中加入一个 task 时,就相当于给 Event Loop 多加了一段支路。在某个时间点,浏览器跟 Event Loop 说,老兄,我这有 task 需要完成。Event Loop 回复说,没问题,把它加到我的 todo 列表中。
考虑执行以下两个函数:
setTimeout(callback1, 1000);
setTimeout(callback2, 1000);
如前面所说,这两个函数先分别在两个平行的 thread 中执行,当时间到了 1000ms 后,它们回到 main thread,把task 放入 queue 中。这时候浏览器告诉 Event Loop,我这有两个 task 需要在 main thread 中执行。Event Loop 就依次执行两个 task。




The Render Steps
当要考虑到渲染的时候,就更加复杂了。即浏览器考虑是否更新当前显示的页面。

渲染就相当于又多加了一条支路,这条支路上有三个 task:
- S:Style calculation, 计算所有的 css 样式,每一个 element 应用什么 css。
- L:Layout,布局会构建一个渲染树,包括当前页面的所有元素和它们所在的位置。
- P:Pixel data,计算页面上的每个像素点数据
假如 浏览器需要 更新页面的时候,它就会对 Event Loop 说,老兄,我需要你帮忙更新页面,这个时候 Event Loop 就会走页面更新的那条支路。

RequestAnimationFrame 的执行顺序
requestAnimationFrame()
方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。

由上图可知,RequestAnimationFrame 函数在渲染的部分执行,这样当浏览器想要更新动画的时候,无需先将 task 加入 queue 中,再告诉 Event Loop 需要更新页面。加快了动画更新的响应速度。
⚠️ 注意:并不是所有的浏览器都像 Chrome 和 FireFox 将 RAF 函数放在 render 的第一步执行,有些浏览器将它放到了 Pixel Data 之后执行。虽然这样做是不正确和不好的,因为已经把页面渲染 完成了,在执行 RAF 函数,那它的效果只能在下一桢看到了。

参考资料:
网友评论