乐趣区

Snabbdom.js(三)

总共写了四篇文章(都是自己的一些拙见,仅供参考,请多多指教,我这边也会持续修正加更新)

介绍一下 snabbdom 基本用法
介绍一下 snabbdom 渲染原理
介绍一下 snabddom 的 diff 算法和对 key 值的认识
介绍一下对于兼容 IE8 的修改

这篇主要是说一下 snabbdom 的 diff 算法
在上一篇中我总结过:对比渲染的流程大体分为 1. 通过 sameVnode 来判断两个 vnode 是否值得进行比较 2. 如果不值得,直接删除旧的 vnode,渲染新的 vnode3. 如果值得,调用模块钩子函数,对其节点的属性进行替换,例如 style,event 等;再判断节点子节点是否为文本节点,如果为文本节点则进行更替,如果还存在其他子节点则调用 updateChildren,对子节点进行更新,更新流程将会回到第一步,重复;
这篇文章的重点就是说一下 updateChildren 这个函数
sameVnode
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
这是一个比较两个 vnode 是否相似,是否值得去进行比较的函数,那么这里为什么会提到它?因为这里面有一个很重要的值 —key 在平时的使用中几乎用不到这个 key 值,不会去专门给它一个定义值,因为 undefined===undefined,不会影响其比较;
key 的作用
key 值的出现主要是为了应付一些场景例如:
<ul> <ul>
<li>1</li> <li>2</li>
<li>2</li> –> <li>3</li>
<li>3</li> <li>4</li>
</ul> </ul>
对于这种情况,如果按照正常的做法,就是一个个 vnode 去进行比较,发现其文本节点不对,就会一个个进行替换例如:<li>1</li>–><li>2</li> …. 这样就会进行三次 dom 操作
对于这种情况是否可以优化呢?答案是可以的,我们可以删除 <li>1</li>,然后添加一个 <li>4</li>,这样就只进行了两次 dom 操作就完成了需要的效果
那这里就涉及到一个标记值,标记着在新 vnode 中还有哪些旧的 vnode 存在,key 值就是充当着这个角色。
[1(key:a),2(key:b),3(key:c)]
[2(key:b),3(key:c),4(key:d)]
[a,b,c] -> [a(x),b,c,d(+)] === [1,2,3] –> [1(x),2,3,4(+)]
key 值与 vnode 形成了一个映射,可以看到,我们通过对 key 值的排序、增删间接完成了对 vnode 的操作,使用最少的 dom 操作来完成了
如何对 key 值进行排序,增删
那这里就会有一个问题,我们如何完成上面的操作呢?这个过程我们可以理解为一种优化对比渲染的过程,也就是 diff 算法的核心
建议大家先看一下这一篇文章,图文并茂
我这边举一个复杂的例子, 记录每一步的操作:
下面是页面真实的 dom,分别保存在自己 vnode 的 elm 属性上;旧 –> 新
<ul> <ul>
<li>a</li> <li>a</li>
<li>b</li> <li>d</li>
<li>c</li> <li>f</li>
<li>f</li> <li>h</li>
<li>e</li> –> <li>k</li>
<li>d</li> <li>b</li>
<li>g</li> <li>g</li>
</ul> </ul>
假设每个元素都有一个 key 值一一对应,且不重复,它们的 key 值分别为
a:1 a:1
b:2 d:6
c:3 f:4
f:4 –> h:8
e:5 k:9
d:6 b:2
g:7 g:7
将旧新 vnode 分别放入两个数组
old:[vnode,….]
new:[vnode,….]

其实我们是比较其 key 值是否相等,然后再决定如何排序,增删 vnode 的位置,patch vnode,最终达到改变 dom 的目的,为了方便理解,我这里把其 key 值拿出来放入一个数组,每一个 key 在数组中的索引都对应着相应的 vnode 在其数组中的索引,在真实代码中是直接比较 vnode.key 值。
oldKey:[1,2,3,4,5,6,7]

oldStartIdx:0
oldStartVal:1

oldEndIdx:6
oldEndVal:7

newKey:[1,6,4,8,9,2,7]

newStartIdx:0
newStartVal:1

newEndIdx:6
newEndVal:7

用的是双指针的方法,头尾同时开始扫描;循环两个数组,循环条件为(old_startIndex <= old_endIndex && new_startIndex <= new_endIndex)
(下面说的 patch 是直接对 vnoe.elm 进行修改, 调用前面的 patchVnode 函数,也就是直接对页面的 dom 进行修改,及时比较及时修改)

比较 oldStartVal 和 newStartVal 是否相等,如果相等则 oldStartIdx 和 newStartIdx 分别加 1,并对 oldStartVal 对应的 vnode 进行 patch,进入下一次循环;这个例子中 oldStartVal==newStartVal,所以 oldStartIdx:1 newStartIdx:1;若不相等,继续比较;
比较过后:
oldStartIdx:1 oldEndIdx:6
oldStartVal:2 oldEndVal:7

newStartIdx:1 newEndIdx:6
newStartVal:6 newEndVal:7

比较范围缩小后:

oldKey:[2,3,4,5,6,7]
newKey:[6,4,8,9,2,7]

dom:
<li>a</li>
<li>b</li>
<li>c</li>
<li>f</li>
<li>e</li>
<li>d</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,f,e,d,g]

比较 oldEndVal 和 newEndVal 是否相等,如果相等则 oldEndIdx 和 newEndIdx 分别减 1,并对 oldEndVal 对应的旧 vnode 进行 patch,进入下一次循环;这里 oldEndVal==newEndVal, 所以 oldEndIdx:5 newEndIdx:5;若不相等,继续比较;
比较过后:
oldStartIdx:1 oldEndIdx:5
oldStartVal:2 oldEndVal:6

newStartIdx:1 newEndIdx:5
newStartVal:6 newEndVal:2

比较范围缩小后:

oldKey:[2,3,4,5,6]
newKey:[6,4,8,9,2]

dom:
<li>a</li>
<li>b</li>
<li>c</li>
<li>f</li>
<li>e</li>
<li>d</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,f,e,d,g]

比较 oldStartVal 和 newEndVal 是否相等,如果相等则 oldStartIdx 和 newEndIdx 分别加 1 和减 1,oldStartVal 对应的 vnode 移动到 oldEndVal 对应的 vnode 后面,并对移动的 vnode 进行 patch,进入下一次循环;这里 oldStartVal==newEndVal, 所以 oldStartIdx:2 newEndIdx:4;若不相等,继续比较;
比较过后:
oldStartIdx:2 oldEndIdx:5
oldStartVal:3 oldEndVal:6

newStartIdx:1 newEndIdx:4
newStartVal:6 newEndVal:9

比较范围缩小后:

oldKey:[3,4,5,6]
newKey:[6,4,8,9]
dom:
<li>a</li>
<li>c</li>
<li>f</li>
<li>e</li>
<li>d</li>
<li>b</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,f,e,d,g]

比较 oldEndVal 和 newStartVal 是否相等,如果相等则 oldEndIdx 和 newStartIdx 分别减 1 和加 1,oldEndVal 对应的 vnode 移动到 oldStart 对应的 vnode 前面,并对移动的 vnode 进行 patch,进入下一次循环;这里 oldEndVal==newStartVal, 所以 oldEndIdx:4 newStartIdx:2;若不相等,继续比较;
比较过后:
oldStartIdx:2 oldEndIdx:4
oldStartVal:3 oldEndVal:5

newStartIdx:2 newEndIdx:4
newStartVal:4 newEndVal:9

比较范围缩小后:

oldKey:[3,4,5]
newKey:[4,8,9]

dom:
<li>a</li>
<li>d</li>
<li>c</li>
<li>f</li>
<li>e</li>
<li>b</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,f,e,d,g]

若不满足上述判断条件,查找 newStartVal 对应的 vnode 是否存在于旧 vnode 数组中。若存在,移动这个旧的 vnode 到 oldStartVal 对应的 vnode 前面,并对这个移动的 vnode 进行 patch,在旧的 vnode 数组中将其原来的位置置为 undefined,并且 newStartIdx 加 1;
比较过后:
oldStartIdx:2 oldEndIdx:4
oldStartVal:3 oldEndVal:5

newStartIdx:3 newEndIdx:4
newStartVal:8 newEndVal:9

比较范围缩小后:

oldKey:[3,4,5]
newKey:[8,9]

dom:
<li>a</li>
<li>d</li>
<li>f</li>
<li>c</li>
<li>e</li>
<li>b</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,undefined,e,d,g]

若不存在,则将这个 newStartVal 对应的 vnde 添加到 oldStartVal 对应的 vnode 前面,并且 newStartIdx 加 1;
比较过后:
oldStartIdx:2 oldEndIdx:4
oldStartVal:3 oldEndVal:5

newStartIdx:4 newEndIdx:4
newStartVal:9 newEndVal:9

比较范围缩小后:

oldKey:[3,4,5]
newKey:[9]

dom:
<li>a</li>
<li>d</li>
<li>f</li>
<li>h</li>
<li>c</li>
<li>e</li>
<li>b</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,undefined,e,d,g]

这里循环了两次
比较过后:
oldStartIdx:2 oldEndIdx:4
oldStartVal:3 oldEndVal:5

newStartIdx:5 newEndIdx:4
newStartVal:undefined newEndVal:9

比较范围缩小后:
oldKey:[3,4,5]
newKey:[]

dom:
<li>a</li>
<li>d</li>
<li>f</li>
<li>h</li>
<li>k</li>
<li>c</li>
<li>e</li>
<li>b</li>
<li>g</li>

oldVnodeArray: 旧的 vnode 数组
[a,b,c,undefined,e,d,g]

循环结束,判断新旧 vnode 的 key 值哪个遍历完,如果旧的便利完,若旧 vnode 数组遍历完, 则将剩余的新 vnode 数组中的 vnode 进行添加;若新 vnode 数组遍历完,则删除剩余的旧 vnode 数组中的 vnode 在上面例子中,我们需要删除 oldVnodeArray 中的三个 vnode,索引分别为 3,4,5,从而删除了 vnode 对应的 elm 最后得到最终的 dom 结构
<ul>
<li>a</li>
<li>d</li>
<li>f</li>
<li>h</li>
<li>k</li>
<li>b</li>
<li>g</li>
</ul>

上面的例子没有将所有情况全部归纳进来,不过应该包含了大部分情况了。还需要注意的就是:

上面只是提到了 key 值,其实比较两个 vnode 是否相似还有一个 sel 属性,必须要两个都相等才行
正常情况下 key 值用到的地方也是 ul-li tr-td 这种子元素重复的场景,因为这种情况下才会涉及到子元素顺序改变还能复用

通过上面的分析,其实还可以发现一个 key 值的特点,就是唯一性和一一对应性。唯一性好理解,毕竟 key 值就是用来每个 vnode 自己的标示;一一对应代表着是你旧 vnode 和新 vnode 中如果没有改变,则其 key 值应保持不变,之所以要提这个是因为很多地方看到了进行循环渲染的时候其 key 值都是用的数组的 index 进行赋值
如果考虑这种情况
<li>a</li> <li>c</li>
<li>b</li> –> <li>a</li>
<li>c</li> <li>b</li>

一般这种 dom 结构都是放在数组里面循环输出的,如果它们的 key 值是按照 index 进行赋值的话,就需要这个地方需要进行三次 dom 操作,就是依次修改其节点的文本值;
[a(1),b(2),c(3)]

[c(1),a(2),b(3)]

那如果我使得它们改变前面对应的元素的 key 值不改变的,一一对应的话,这里只需要依次 dom 操作,就是把 <li>c</li> 移动到最前面
[a(1),b(2),c(3)]

[c(3),a(1),b(2)]

退出移动版