halo,大家好久不贱
之前,我说,react 16 没有 diff,一堆人反驳我,甚至还有人给我截源码……
不过也确实,react 16 的算法,没找到特别合适的文章,看源码又十分要命
所以其实 fre 的排位算法,其实是大部分凭空复现,不过基本的精髓也有了
前提
在阅读本篇文章之前,最好是对传统的 diff 有了解,没了解也没关系,只需要知道,它是俩 vdom 对比,而 children 的 比对是俩数组
然后数组就有索引,遍历完一个索引,就删掉,进行下一轮
但是 react 16 由于 Fiber 特殊的遍历形式,导致,不再有新旧 vdom 了
在 Fiber 的遍历过程,只有一个 fiber 节点在不断更替,我们通常称它为 WIP
然后就拿这个 WIP 去和 新的 vdom 比对
局限
拿一个节点和一个嵌套的对象进行比对,很明显,想遍历没法遍历,想标号没法标号,可以说是没法儿比对的
所以我们想了个办法,首先,将 vdom 转化成 fiber
准备工作
这个转换很简单,就是给 vdom 配上 child sibling return,就可以
比如:vdom 长这样:
let vdom = {
type:'div',
children:[
{
type:'h1'
}
]
}
复制代码
它需要被拍平,变成一个 fiber 节点,children 的 第一个为 child,第二个 为 child 的 sibling
let fiber = {
type:'div',
child:{
type:'h1'
},
//...
}
复制代码
然后这样,就变成 两个 fiber节点进行比对了,很不幸,还是不行,因为即便是拍平成一个对象了,这个对象没法儿遍历,不是我们想要的
所以我想了个更骚的方法,和 effectLink 一样,对 child 进行收集
每个 fiber 下面都有一个 childFibers 对象,它包含这个 fiber 下面所有的 child
let fiber = {
type:'div',
child:{
type:'h1'
},
childFibers :{child1,child2,...childs}
//...
}
复制代码
到这里,很多人会问,为什么不直接把 vdom 的 children 放上去?为什么需要是对象而不是数组?
我们看一个用例:
A B C -> A C
复制代码
一旦有新增或者删除行为,如果是数组,它的索引会发生变化,比如,C 原来是 [2],因为 B 删掉了,C 变成 [1],B 找不到了
如果说这里有一一对应的遍历行为,也就是传统 diff,可以通过 index++ 或者 delete xxx 的方式控制索引的增减
所以关键点来了,fiber 并没有类似的遍历,自然控制不了索引
所以就需要对节点进行标号,对位置进行重排,这就是 react 16 算法的关键所在
没错,【排位】是关键,【比对】是自然发生的
我们先给节点标号,fre 用了一个巧妙的 hash 去标记
children:{.0:child1,.1:child2,...childs}
复制代码
我手动给他标号,就不用担心索引的问题了,因为手动写死的标号,不会因为第二轮遍历而发生变化
ps. 这里不能用数字作为键值,所以隐式转换成 hash
然后,针对新增和删除的行为,就这么愉快的搞定了
接下来就要搞更难的,位置的调换,比如这个用例:
A B -> B A
复制代码
没有新增和删除,那么默认是更新,如果更新的话,要更新两次,但是很明显,只需要 B 插入 A 就可以了
我们要想知道,那些节点需要更换位置,需要对位置进行记录,所以每个 fiber 上,都有个 index
这里 A 的 index 是 0 变为 1,位置改变,需要插入,而不是更新,B 也是如此
但是不能插两次,所以我们规定,如果是是被插者,就不能插别人……
到这里,思考一下下面的用例:
A B C D -> B A D C
复制代码
首先,B 插 A,A 被插了,不动,然后 C 插 D ,D 被插了,不动
很好,只需要两次,接着看:
A B C D -> C D A B
复制代码
根据上面的算法,我们只能知道,C 插 D 和 B 插 A ,并不能知道 C D 和 A B 要不要换
所以我们再规定,如果说被插者和兄弟是一个人,就不能插,需要换个人
比如这里的 C 本来要插 D,但是发现 D 是自己的兄弟,那就换插 A(firstChild)
所以在这个用例中,C 插 A,D 也 插 A,更新两次,没毛病
至此,整个排位算法就差不多啦
具体的实现其实还是要考虑很多边界的,其实算法这东西,看实现和没意思
搞懂了本质,自己复现就很简单啦~
总结
react 16 以后的算法,已经不再是 diff 了,官方所有的源码和注释中,从未出现过 diff 这个单词
但是传统的 diff,如 inferno 等,仍然值得我们研究,毕竟 Fiber 其实是个有风险的方案,很难驾驭
之后看看有机会分享一下 inferno 的算法~
最后放上 fre 的 地址:github.com/132yse/fre
欢迎 star 与 fork ~