10

fre2 Fiber(时间切片+超时队列)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/337723841
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

fre2 Fiber(时间切片+超时队列)

前端玩票,高产玩具,fre,fard,berial,ep……

泥谋嚎鸭!俺是惊奇小朋友 132 本伊,最近很多人对 fre2 的新的 Fiber 架构感兴趣,我准备写一篇文章详细来说

从浏览器说起

这块内容可能优点碎碎念,实际上浏览器一个页面,包含很多东西,比如 event loop,raf 和 layout/paint 的安排

我一直不提倡一些公司出 event loop 有关的面试题,因为实际上面试题只是 js 语言层的 event loop,而浏览器中的 event loop 更重要的是浏览器层的行为

概括来说,event loop,raf 在浏览器层就三点:

1. microtask 会在浏览器绘制前 [同步清空] 队列
2. raf 会在浏览器绘制前 [异步按帧清空] 队列
3. macrotask 会在浏览器绘制 [后] 同步出队一次 

浏览器层的协程

很多人对听到协程这个词,会觉得很有意思,因为对 js 语言来说,协程一直是语言或引擎级别的,比如 generator,我想

感兴趣的是这种

但是对于前端框架来说,这个“协程”却不在于 js 线程,它只针对浏览器,说大白话就是,我就是想让浏览器不阻塞

这个调度和 js 语言,和 event loop 在 js 线程的行为,其实关系不大

好了,大家已经知道了,我们做的是浏览器层的协程,那么我们怎么实现它呢?

主要思路有两种:

1. 借助 macrotask 在浏览器层的表现(上文中的 3)
2. 借助 raf 的异步行为(上文中的 2)

严格来说,我们既然做的是浏览器的调度,那么使用 raf 是最好的,因为 raf 只属于浏览器层,但是 raf 太受帧率影响了,低端设备直接就凉凉了……

于是无论是 react 还是 fre,都利用了 macrotask 来实现,通俗地讲

layout
setTimeout(A)
layout
setTimeout(B)

就是这样,讲每个组件的更新放到 setTimeout 中,那么组件更新的间隙浏览器就有机会绘制

时间切片

了解了上面这一堆,我们先说最硬核的,也就是时间切片

你可以理解为,将所有的 element 更新都放到 setTimeout 里,然后我每 16ms 就暂停一下,然后浏览器在这个时候就插入绘制一下,完了以后我继续 setTimeout

这个的阈值是 16ms,单位是 element,在这 16ms 中,有可能更新了 10 个元素,有可能 20 个

这个的前提是,元素与元素之间是可以被打断的,树这个结构,如果你用递归是一定无法打断的,于是 react 使用链表去描述这棵树,这样一来,对链表的循环就可以打断了

时间切片我就不啰嗦了,react 捣鼓了这么多年,大家应该都倒背如流啦

大家只需要记住这个实现的两个重点就好了

1. setTimeout
2. 链表

优先级

如果说 fre 和 react 在时间切片上的实现是一样的,那么 fre2 比 react 拉开差距的就是优先级和超时队列了

一个很经典的问题:react 为什么每次都从 root 开始遍历?

因为 react 每次更新都给组件分配了优先级,高优先级的组件先更新,但是如果多个组件同时更新,我怎么才能知道哪个组件的优先级更高呢?

思路无非有两个

1. 将同时更新的组件推入一个队列,然后对队列排序
2. 不进入的队列,直接在树上标记优先级,然后对这个树排序

react 选择了第二种,也就是不使用队列,直接对整棵树全部遍历,基于树结构进行排序的好处是,优先级可以叠加

父组件的优先级是子组件的冒泡叠加,不同行为的优先级彼此叠加,可操作空间更大

这块非常令人费解,之后我再写一篇文章,我们今天主要谈它的缺点

基于优先级树的调度有很多问题,一方面它使得 react 代码仓库变成一座 shi 山;另一方面,也是最致命的,那就是对第三方库的破坏

如果你不懂内部调度,那么你写的同步库多半是有 bug 的,比如 mobx 作者就哭晕在厕所

但是真正懂这个调度的,又有几个人呢?

所以,fre 大胆地使用了思路 1 ,也就是将多个同时更新的组件推入队列,然后对这个队列进行排队

超时队列

说了这么多,终于到今天的主角啦!

超时队列说白了就是对 [悬挂组件] 的另外一种时间切片,举个例子

1. 比如现在 a b c d 四个组件同时更新,它们会同时被推入队列中
2. a 更新,发现超时了,那么 a 先到一边,b 先更新
3. b c d 都是轻量任务,更新完了也没超时
4. 最后更新 a

以上,每一轮更新,超时时间都会递增,然后每一轮都是在 setTimeout 中更新的,浏览器有绘制的机会,取决于超时时间

超时时间的计算来自物理排队论,公式只是表面,反正我们目的是最紧凑地安排更新

超时队列是我根据 fre 的现状和优先级的缺点,创造的调度策略,一方面是因为 fre 没有合成事件,做不了优先级的划分,优先级是根据事件名写死的,比如 click 比 scroll 的要搞

另一方面是因为,优先级调度是一种“抢占式”的调度,这不符合携程的概念,超时队列是一种“妥协式”的调度,更符合 Fiber 和协程的概念

除此之外,超时队列的实现可以和时间切片一起抽象,最终代码量非常少

可以说超时队列是最适合 fre 的调度策略

tearing

感谢

提出来的问题,tearing 是无论是优先级还是超时队列都会出现的一系列问题,这也是为什么 mobx 等同步库无法解决的最大问题没有之一

说白了,就是当你的组件更新到一半,此时有更高的优先级过来,或者说此时已经超时了,那么这个组件的状态将会出现上面一部分是新状态,下面是旧状态

这是由于时间切片的单位是 element 导致的,我们解决这个问题只有两条路

1. 如果一个组件被打断,那么则将状态重置为旧状态,下次重新更新
2. 被打断的组件,保存一个快照,也就是更新到了按个 element,下次从这个 element 继续更新

方案 2 是最理想的方式,也就是组件本身会被切片,fre2 第一个版本我会使用方案 1,未来探讨方案 2

补充

以后写文章就加一个区块,专门放我不知道往哪儿搁的内容

问:什么时候存在多个组件同时更新的情况?

答:fetch 的时候,网络请求是异步的,完全有可能多个组件同时返回结果

问:超时队列和 vue 的 microtask 有啥区别

超时队列根据超时时间对这个队列进行切片和重新排序,是另一种时间切片,不阻塞浏览器

vue 的 microtask,是同步清空队列,会一直阻塞浏览器

总结

最后放一下 fre 的 github 地址

https://github.com/yisar/fre

fre2 目前正处于原型阶段,除了超时队列,另一个重大更新是新的 diff 算法,我首次将 [ 两端遍历 ] 的算法引入链表的遍历中

说实话比 react/fre1 的算法要好得多,但是我也不想和 react 比谁更强

我只想寻找一群对 Fiber,对协程,对 diff 感兴趣的小伙伴

不行吗!不行吗!不行吗!

不行我就去碎啦!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK