vue3不经过编译优化的diff
在看hcy的vue源码解析, 看完了渲染器部分, 简单总结下vue3的diff流程. (作为之后复习用, 并不作为学习.)
背景介绍
vue的大致流程是: 把一些数据变成响应式, 在数据变化的时候去更新对应的视图.
视图是由vnode产生的, vue会保存一份vnode, 在之后更新时与新产生的vnode对比来尽量找到最小的变化, 并把这变化执行到真实dom上.
对比的过程大致是这样:
- 对比type, type不同就直接unmount老的, mount新的.
- type相同, 对attributes进行修改.
- attributes种类繁多, 要对class, style, 事件做处理.
- diff子节点. 子节点有2个类型, 字符串和数组.
- 新老子节点非同一个类型, 就直接unmount老的, 遍历新的并mount.
- 新老子节点都是数组, 才是本文的主题: diff.
因为书还没讲到compiler, 所以compiler做的优化这里还不包括, 可以认为这个diff流程是手写render function产生的vnode的diff.
另外, 这里考虑的是数组被key的情况, 如果数组没有被key, 不会有diff过程, 只是简单地将公共长度部分做patch, 如果新的数组长, 就mount, 如果旧的数组长, 就unmount.
下面进入主题: patchKeyedChildren
diff流程
源码的注释写得非常体贴, 所以我就模仿源码中的注释来写例子.
去头去尾
(a b) i j k (c d)
(a b) x y z (c d)
找出头尾可以复用的dom, 只patch他们的attributes, 减少diff范围.
具体方法:
两次遍历. 分别用1跟指针和2跟指针(数组长度可能不同, 去尾的时候需要2跟指针)
循环判断指针节点是否可复用.
如果可以, patch他们的attributes, 并移动指针.
如果不可以复用. 停止指针.
最后得到3个指针. 来判断需要进一步diff的内容.
简单的情况: 纯新增或减少
(a b) c
(a b)
或
(a b)
(a b) c
需要diff的内容有一边是完全没有的情况, 只需要新增或卸载节点就可以了.
具体方法:
判断指针是否重合, 可以判断出是否有一边的数组被完全处理完了.
如果新数组的指针还为重合, 循环2个指针中间的索引, 逐个新增.
反之逐个卸载.
新老数组都还有长度
a b [c d e] f g
a b [e d c h] f g
面对这2个序列, 我们要做的事有3个:
- 找出可以复用, 并不需要移动的元素, patch他们的attributes.
- 移动可以复用但需要移动的元素, patch他们的attributes.
- 新增或卸载节点.
- 如果需要移动节点, 则移动节点.
具体方法:
- 遍历新数组, 创建一个新数组的key-value的map
keyToNewIndexMap
. - 创建一个数组
newIndexToOldIndexMap
, 长度为新数组的长度, 内容是”新元素在老数组里是第几个”, 初始值为0. 代表”新元素在老数组里不存在” - 遍历老数组, 利用第一步创建的
keyToNewIndexMap
寻找每个老元素是否有对应的新数组. - 如果老元素在新数组中存在, patch这个元素的attributes, 并更新第二步创建的
newIndexToOldIndexMap
. - 如果老元素在新数组中不存在, 则卸载当前老元素.
- 建立一个变量来计数被patch的数量, 如果新元素已经都被patch, 就卸载当前老元素.(这个算算法优化)
- 建立一个变量
moved
, 初始值为false, 如果每次从keyToNewIndexMap
取出的不是递增, 就将moved
设为true, 后续根据moved
来判断是否移动节点. - 至此, 老元素的卸载已完成, 并且我们获得了每个新元素对应了哪个老元素的信息
newIndexToOldIndexMap
. - 从
newIndexToOldIndexMap
里获取一个最长递增子序列. 意义是: 新数组和最长递增子序列重合的部分是不需要移动的. (lss: longest stable subsequence) - 反向遍历新数组. 同时增加一根lss的指针, 一起遍历.
- 如果新元素符合lss, 则不动. 并向上移动lss的指针.
- 如果新元素在
newIndexToOldIndexMap
里的索引是0, 则新增元素. - 如果都不是, 则将这个新元素移动到当前的指针位置.
简单diff和双端diff
书里还介绍了没key时候的diff, vue2的双端diff, 和(我认为是快速diff核心部分雏形的)简单diff.
简单diff
- 遍历新数组, 寻找每个新元素在老数组中的索引, 并记录这个索引.
- 如果下个新元素在老数组中索引比记录的大, 则patch他的attributes.
- 当发现索引不是递增的, 代表这个元素需要被移动, 则移动到合适的位置并patch他的attributes.
- 如果找不到索引, 则新增一个节点放到合适的位置.
- 最后遍历老数组, 把新数组中不存在的老元素卸载掉.
双端diff
- 建立4跟指针: 新老数组的头和尾. 并开始遍历.
- 每次遍历, 4个方案对比指针上的元素是否是同一个元素: 新头-老头. 新尾-老尾. 新尾-老头. 新头-老尾.
- 如果发现是同一个元素, 就patch他的attrbutes, 并把对应的指针向上/向下移动一格.
- 如果是”新尾-老头. 新头-老尾.”这2个情况下命中的, 还需要移动元素到合适的位置.
- 如果4个方案都找不到相同的元素, 就去老数组里找新元素的索引.
- 如果找到, 则把新元素移动到合适的位置, 并patch, 并把旧数组对应的元素置为undefined, 并在循环中判断如果指针对应的元素为undefined, 就移动指针并进入下个循环.
- 如果没找到, 就新增一个节点.
- 如果遍历到”新头指针>新尾指针”并且”老头指针<=老尾指针”的情况, 说明剩下的老元素都要被卸载, 使用老数组的2个指针遍历卸载.
(本文完)
如果你觉得本文对你有帮助, 你可以请我喝一杯咖啡
本文遵循cc协议
你可以在注明出处和非商用的前提下任意复制及演绎