阅读 437

「学习笔记」Jake Archibald: In The Loop

来源

有些小伙伴不是很方便直接观看原视频(因为需要科学上网),所以我在这里做一个记录。加强所学的同时,同时也方便大家。

第一个问题

1.png

这段代码是否存在一个隐患,用户的屏幕会出现一闪而过的情况?答案:不会。原因请看下面👇

主线程

2.png

JS的执行浏览器的渲染DOM存储都在主线程上。这意味着网页上大部分的代码都是有顺序的,不可能存在同时编辑同一个DOM的情况。

但是主线程,如果有的代码运行了很久比如200ms,它会阻塞用户的交互,和渲染是很糟糕的体验。

3.png

所以尽管我们拥有一些主线程,但是希望衍生出一些其他线程,比如用来处理网络请求,监控输入设备。一旦衍生线程,需要页面响应,它们会通知主线程,这就是事件循环干的事情。

setTimeout 是如何工作的?

4.png

5.png

JS不会同步的等待,因为这会阻塞主线程。JS也不会并行,因为这会产生很多竞态。

JS会将任务推送到队列中,然后再回到主线程中。

这也是浏览器的核心工作方式。比如,当发生点击的时候,操作系统会在队列中提交任务。fetch请求的响应也会推送到队列之中。

任务队列 Task Queues

6.gif

在没有事情发生的时候,事件环就是在空转

7.png

我们通过 setTimeout 向队列中添加 callback1任务,callback2任务。任务会在主线程之外,并行等待1000ms,然后回到主线程当中。

8.png

当任务回到主线程后,事件环将会“绕道”去执行任务。

9.gif

浏览器渲染

10.png

我们可以把浏览器的渲染,想象成事件队列上的另一个弯道。包含了CSS样式计算(S),构建渲染树(L), 计算元素的位置像素(P),然后进行绘制。

浏览器渲染的这条弯道,和执行任务(JS代码执行),分别位于事件队列的两端。

11.gif

while(true)

12.png

使用 while(true) 会使浏览器停止,gif图也不在更新,文字也无法选中。

在事件循环中,while(true),会造成什么样的后果呢?

13.png

while(true) 是一个永远无法结束的任务,直到永远。

事件环永远无法绕到,浏览器渲染的那个弯道上。事件环也无法处理,新添加的任务。所以浏览器会变得无响应。

回到第一个问题

14.png

到这里,我们也很好理解了。为什么这段代码,没有闪烁的风险。

因为事件环,必须执行完任务,才会进行浏览器渲染。(任务 --> 渲染 --> 任务 --> 渲染)

就像之前说的那样,执行任务(执行JS),和浏览器渲染是分别位于事件渲染的两端的。任务总是在下一次渲染前,完成

setTimeout 会不会阻塞事件环呢?

15.png

答案是不会,setTimeout添加一个任务后,事件环执行完任务后,会再添加一个任务。

16.gif

而任务不会永远无法结束,所以事件环也有时间去处理浏览器的渲染。

任务中不要放,渲染相关的代码。

在任务中不要放,渲染相关的代码。应该放到渲染之前,requestAnimationFrame (rAF)正是这样做的。

17.png

18.png

19.gif

分别使用 requestAnimationFramesetTimeout 做动画。setTimeout 相比 requestAnimationFrame 更快,这是为什么呢?

因为每当我们执行完一个任务(JS)后,浏览器是不能保证会重新渲染的可能我们执行了多次任务之后,浏览器才会渲染一次

setTimeout(callbak, 0), 就会存在这种情况,callbak执行了多次,浏览器可能只会渲染一次。而在浏览器在后台运行时,callback的执行是没有意义的。

在大多数情况下,我们屏幕的更新频率是60HZ, 如果我们在1s内,更新样式1000次,浏览器不会渲染1000次,只会渲染60次。

20.png

为什么 setTimeout 会更快?因为callback执行的次数,要比 rAF 多。rAF 只会在渲染之前执行回调。

渲染帧

21.png

渲染的过程在每一帧发生之前

22.png

而任务,却会任意出现在帧的任意时刻。很多任务,是得不到浏览器的渲染的。

漂移

23.png

很多老的动画库比如jq,使用1000 / 60, 来避免,在一个帧内执行无用的callback。

24.png

但是setTimout, 可以会出现漂移,比如这个帧内不执行callback,下一个帧内执行两次callback。所以setTimout并不适合用来做动画。

25.png

而使用rAF,callback只会在每一帧渲染前执行callback

一个例子

26.png

通过之前说明,我们可以知道,这个box, 不会先运动到1000px,然后再运动到500px。那么怎么做,才能让box先运动到1000px,然后再运动到500px呢?

27.png

如果这样修改呢,结果会如何?很遗憾,box依然会直接运动到500px的位置。这是为什么呢?请看下面

28.png

29.png

任务(运动到1000px) -> rAF(运动到500px)-> 浏览器渲染

我们需要怎么修改呢?

30.png

我们需要在嵌套一层rAF,事件环的顺序就变成了。任务(运动到1000px) -> 第一层rAF -> 浏览器渲染 -> 第二层rAF(运动到500px)-> 浏览器渲染

除了再嵌套一层rAF,有没有其他方法呢?大家都学习过浏览器的重绘,我们可以使用 getBoundingClientRect, clientWidth 等 API 强制浏览器重绘。

31.png

🤔关于这个例子的问题

我在Chrome80的版本上测试了上面的代码, 结果是不可行的。代码需要调整。原因应该是和 transition 相关,至于为什么我不是很清楚。


btn.addEventListener('click', ()=>{
    box.style.transform = 'translateX(500px)'
    requestAnimationFrame(()=>{
        requestAnimationFrame(()=>{
            box.style.transition = 'transform 1s ease-out'
            box.style.transform = 'translateX(250px)'
        })
    })
})
复制代码

const btn = document.getElementById('btn')
const box = document.getElementById('box')
btn.addEventListener('click', ()=>{
    box.style.transform = 'translateX(500px)'
    box.clientWidth
    box.style.transition = 'transform 1s ease-out'
    box.style.transform = 'translateX(250px)'
})
复制代码

🤔一点个人看法

我们在很多中文文章里,都会把 requestAnimationFrame 归类为宏任务,但是演讲看到这里,我们应该可以看出,requestAnimationFrame 和 宏任务以及微任务是没有关系。requestAnimationFrame 的callback何时执行,只和浏览器何时刷新(刷新前)执行。

Microtasks

Microtasks 到底会在什么时候执行,会在宏任务结束后执行吗?这种说法只能说部分正确。正确的说法应该是JS堆栈为空时执行。

无限循环的微任务

32.png

浏览器一样会被卡住。为什么?如果处理微任务时,有新的微任务被添加,并且加入的速度也大于执行的速度,那么就会永远执行微任务。

一个例子

33.png

当我们点击用鼠标button的时候,log 的顺序是怎么样的?

结果:Listener 1 -> Microtask 1 -> Listener 2 -> Microtask 2

34.png

当我们使用代码点击button的时候,log 的顺序是怎么样的?

结果 Listener 1 -> Listener 2 -> Microtask 1 -> Microtask 2

为什么?

第一个例子

# 第一步
# 第一个回调执行
JS stack: callback1(执行中)
Microtasks: 空
log: 空

# 第二步
# 第一个回调执行完成,并退出JS stack,JS stack为空,此时开始清空微任务队列
JS stack: 空
Microtasks: Microtask 1
log: Listener 1

# 第三步
# 执行第二个callback
JS stack: callback2(执行)
Microtasks: 
log: Listener 1,Microtask 1

………………
复制代码

第二个例子

第二个例子, 与第一个例子有一点的不同,我们是用代码调用的,所以最开始JS堆栈中会多出一个脚本堆栈

# 第一步
# 使用JS调用点击事件,JS stack会多出一个Script
JS stack: Script
Microtasks: 空
log: 空

# 第二步
# callback1 开始执行
JS stack: callback1(执行中), Script
Microtasks: 
log: 

# 第三步
# callback1 执行完成,退出堆栈
# 但是JS堆栈中,还有 Script,所以还不能执行微任务
JS stack: Script
Microtasks: Microtask 1 
log: Listener 1

# 第四步
# 此时 callback2 执行
JS stack: callback2(执行中),Script
Microtasks: Microtask 2, Microtask 1
log: Listener 1 ,Listener 2

# 第五步
# Listener 2 执行完成退出,
JS stack: Script
Microtasks: Microtask 2, Microtask 1 
log: Listener 1 ,Listener 2

# 第六步
# Script 执行完成退出,准备开始清空微任务队列
JS stack: 空
Microtasks: Microtask 2, Microtask 1 
log: Listener 1 ,Listener 2

# 第七步
# 开始清空微任务队列
JS stack: 空
Microtasks: 空
log: Listener 1 ,Listener 2,Microtask 1,Microtask 2
复制代码