从虚拟节点到diff运算,vue2和snabbdom虽有改变,但核心概念和思路比较相似,之前点到过 snabbdom的应用和源码解析,这里讲讲vue2的diff实现

diff存在的意义和思路

我们数据响应式会重新渲染页面,这里我们通过异步更新降低了更新频率,但本质上还是覆盖了DOM操作,更进一步,对新旧两次结果进行比较,尽可能复用节点,而不是操作新的节点。

两个vnode虚拟节点进行比较比较耗时,vue简化成同级比较,实际情况中也是很少出现父类变,子类不变的情况。

同级比较用到了 patch 算法,应用比较简单: patch(oldVnode, newVnode)

思路:

  1. 同级比较,降低复杂度。同级不同子类就不比了
  2. 如何对比?看标签类型
    1. 不一样,替换
    2. 一样,下一步,起码可以复用标签
    3. 虽然标签类型一样但都是undefined,文本节点替换
  3. 标签一致,看具体属性
    1. 属性多了,补充
    2. 属性少了,删除
  4. 比较子代
    1. 有无子代,变化了就算了
    2. 都有子代,下一步
  5. 既然都有子代,那就要好好比比了,最核心的部分

子代都是数组:

  • a b c d
  • a b c d e

我们准备双指针:

  • old-start-index old-end-index
  • new-start-index new-end-index

后续的思路就是移动指针,通过指针能够获得具体的节点,进行比较。

用到了while迭代,索引移动。while运行的条件是 开始和结束节点不交叉:

  1. oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex

伪代码流程

1 比较头部:

  • old-start-index 代表的节点和 new-start-index 代表的节点一样
  • old-start-index++new-start-index++ 移动节点
  • 本次迭代完成
  • 最后剩下的比如多了或者少了,就操作dom
    1. if(isSameVnode(oldStartVnode,newStartVnode)){
    2. patch(oldStartVnode,newStartVnode);
    3. oldStartVnode = oldChildren[++oldStartIndex];
    4. newStartVnode = newChildren[++newStartIndex];
    5. // 优化向前追加逻辑
    6. }

场景1:尾结点增加

  • a b c d
  • a b c d e

2 比较尾部

  • 如果上面判断不成立,比较尾部
  • 尾部比较,索引移动
  1. if(isSameVnode(oldEndVnode,newEndVnode)){
  2. patch(oldEndVnode,newEndVnode); // 比较孩子
  3. oldEndVnode = oldChildren[--oldEndIndex];
  4. newEndVnode = newChildren[--newEndIndex];
  5. }

场景2:头节点增加

  • e a b c d
  • a b c d

3 比较首尾

  • 如果上面判断不成立,比较旧头、新尾
  • 如果匹配上,操作dom
  • 然后移动两遍的指针
  1. if(isSameVnode(oldStartVnode,newEndVnode)){
  2. patch(oldStartVnode,newEndVnode);
  3. parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
  4. oldStartVnode = oldChildren[++oldStartIndex];
  5. newEndVnode = newChildren[--newEndIndex]
  6. // 尾部移动到头部
  7. }

4 比较尾首

  • 如果上面判断不成立,比较旧尾,新头
  • 如果匹配上就操作dom
  • 然后移动指针
    1. if(isSameVnode(oldEndVnode,newStartVnode)){
    2. patch(oldEndVnode,newStartVnode);
    3. parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
    4. oldEndVnode = oldChildren[--oldEndIndex];
    5. newStartVnode = newChildren[++newStartIndex]
    6. }

    5 暴力比对

    如果上述条件没配上,就进入这里了,虽然首尾不一样,挑挑拣拣里面一样的还是尽量复用

思路:

  • 旧节点维护map={key:index}
  • 新节点去找key,找到了就移动,并且原位置设置null
  • 找不到就新建
  • 最后剩下的删除和补充
    1. let moveIndex = map[newStartVnode.key];
    2. if (moveIndex == undefined) { // 老的中没有将新元素插入
    3. parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
    4. } else { // 有的话做移动操作
    5. let moveVnode = oldChildren[moveIndex];
    6. oldChildren[moveIndex] = undefined;
    7. parent.insertBefore(moveVnode.el, oldStartVnode.el);
    8. patch(moveVnode, newStartVnode);
    9. }
    10. newStartVnode = newChildren[++newStartIndex]

以上就是最简化版本的过程了