统共写了四篇文章(都是本身的一些鄙见,仅供参考,请多多指教,我这边也会延续修正加更新)
这篇主假如说一下snabbdom的diff算法
在上一篇中我总结过:
对照衬着的流程大致分为
1.经由历程sameVnode来推断两个vnode是不是值得举行比较
2.假如不值得,直接删除旧的vnode,衬着新的vnode
3.假如值得,挪用模块钩子函数,对其节点的属性举行替代,比方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)]