vue diff算法

在更新dom的时候vue并不是实时更新真实dom的变化,而是通过对比oldvnode和newvnode两个虚拟dom来进行最小化的更新视图

更新的时候遵守同级对比,不能跨层,以下是图的示例

具体更新规则如下,依次判断

  1. oldvnode和newvnode都有文本节点,则用新的文本节点替换旧文本节点
  2. oldvnode没有子节点,newvnode有子节点,则添加新的子节点
  3. oldvnode有子节点,newvnode没有子节点吗,则删除旧的子节点
  4. oldvnode和newvnode都有子节点,则updateChildren()方法

具体patch代码如下:

import createElement from "./createElement";  //创建一个真实的dom
import vnode from "./vnode";   //生成虚拟dom
import updateChildren from "./updateChildren";
export default function(oldVnode, newVnode) {
  //判断传入的第一个参数,是DOM节点还是虚拟节点
  if(oldVnode.sel == '' || oldVnode.sel == undefined) {
    //传入的第一个参数是DOM节点,此时要包装为虚拟节点
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [],undefined,oldVnode)
  }
  //判断oldVnode和newVnode是不是同一个节点
  if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
    //判断是不是同一个对象
    if(oldVnode == newVnode) return
    //判断新vnode有没有text属性
    if(newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
      //新vnode只有text属性
      if(newVnode.text != oldVnode.text) {
        //新虚拟节点和老虚拟节点的text不同直接替换掉
        oldVnode.elm.innerText = newVnode.text
      }
    }else {
      //新vnode没有text属性,表示有children属性,挂有子节点
      if(oldVnode.children !== undefined && oldVnode.children.length > 0) {
        //老的有children,复杂情况
        updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
      }else {
        //老的没有children,新的有children
        //清空老的节点内容
        oldVnode.elm.innerHTML = ''
        //遍历新的vbode的子节点,创建dom上树
        for(let i = 0; i < newVnode.children.length; i++) {
          let dom = createElement(newVnode.children[i])
          oldVnode.elm.appendChild(dom)
        }
      }
    }
  }else {
    let newVnodeElm = createElement(newVnode) //第二次更新的时候是这个让vnode有了elm属性
    //插入到老节点之前
    if(oldVnode.elm.parentNode !== undefined && newVnodeElm){
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
    }
    oldVnode.elm.parentNode.removeChild(oldVnode.elm)
  }
}

updateChildren的时候首先遵循四种命中策略

依次是创建四个指针,指向newvnode的children的头节点(新前)、尾节点(新后),oldvnode的children的头节点(旧前)、尾节点(旧后)。

然后根据四个指针循环判断

  1. 新前与旧前是否命中(标签名相同,key相同),如果命中则进行patch,在patch完成之后,需要移动指针,新前+1,旧前+1,指针靠拢,如果未命中则判断以下
  2. 新后与旧后是否命中,如果命中则进行patch,在patch完成之后,需要移动指针,新后-1,旧后-1,指针靠拢,未命中则判断以下
  3. 新后与旧前是否命中,如果命中则进行patch,在patch完成之后,不同于1、2命中方式,需要移动真实的dom,因为dom结构已经发生了变化,接着移动指针,新后-1,旧前+1,指针靠拢,还未命中则继续以下
  4. 新前与旧后是否命中,如果命中则进行patch,在patch完成之后,需要移动指针,新前+1,旧后-1,指针靠拢,还未命中则继续以下

则需要创建一个map保存oldvode的children的子节点的key和数组位置idx的映射关系,然后找到新前的key对应map的idx,如果没找到则说明是新节点,直接插入到oldStartVnode.elm之前,如果找到则进行patch然后将oldChild队形的idx项置为undefined,用于下次循环的时候略过。

如果循环结束,newStartIdx <= newEndIdx或者oldStartIdx <= oldEndIdx 则添加或删除节点

具体代码如下

import createElement from "./createElement";
import patch from "./patch";
export default function updateChildren(parentElm, oldCh, newCh) {
  // 四个指针
  // 旧前
  let oldStartIdx = 0;
  // 新前
  let newStartIdx = 0;
  // 旧后
  let oldEndIdx = oldCh.length - 1;
  // 新后
  let newEndIdx = newCh.length - 1;

  // 指针指向的四个节点
  // 旧前节点
  let oldStartVnode = oldCh[0];
  // 旧后节点
  let oldEndVnode = oldCh[oldEndIdx];
  // 新前节点
  let newStartVnode = newCh[0];
  // 新后节点
  let newEndVnode = newCh[newEndIdx];
  //map映射
  let keyMap = null;

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
    if (oldCh[oldStartIdx] === undefined) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldCh[oldEndIdx] === undefined) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newCh[newStartIdx] === undefined) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newCh[newEndIdx] === undefined) {
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(oldStartVnode, newStartVnode)) {
      // 新前与旧前
      console.log("新前与旧前命中");
      // 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
      patch(oldStartVnode, newStartVnode);
      // 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
      // 新后与旧后
      console.log("新后与旧后命中");
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(oldStartVnode, newEndVnode)) {
      // 新后与旧前
      console.log("新后与旧前命中");
      patch(oldStartVnode, newEndVnode);
      // 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面
      // 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
      // 新前与旧后
      console.log("新前与旧后命中");
      patch(oldEndVnode, newStartVnode);
      // 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
      // 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 四种都没有匹配到,都没有命中
      console.log("四种都没有命中");
      // 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了
      if (!keyMap) {
        keyMap = {};
        // 记录oldVnode中的节点出现的key
        // 从oldStartIdx开始到oldEndIdx结束,创建keyMap
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key;
          if (key !== undefined) {
            keyMap[key] = i;
          }
        }
      }
      // 寻找当前项(newStartIdx)在keyMap中映射的序号
      const idxInOld = keyMap[newStartVnode.key];
      if (idxInOld === undefined) {
        // 如果 idxInOld 是 undefined 说明是全新的项,要插入
        // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
      } else {
        // 说明不是全新的项,要移动
        const elmToMove = oldCh[idxInOld];
        patch(elmToMove, newStartVnode);
        // 把这项设置为undefined,表示我已经处理完这项了
        oldCh[idxInOld] = undefined;
        // 移动,调用insertBefore也可以实现移动。
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
      }
      // newStartIdx++;
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // 循环结束
  if (newStartIdx <= newEndIdx) {
    // 说明newVndoe还有剩余节点没有处理,所以要添加这些节点
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      console.log('添加节点')
      // insertBefore方法可以自动识别null,如果是null就会自动排到队尾,和appendChild一致
      parentElm.insertBefore(createElement(newCh[i]), null);
    }
  } else if (oldStartIdx <= oldEndIdx) {
    // 说明oldVnode还有剩余节点没有处理,所以要删除这些节点
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      console.log('删除节点')
      if (oldCh[i]) {
        parentElm.removeChild(oldCh[i].elm);
      }
    }
  }
}

// 判断是否是同一个节点
function checkSameVnode(a, b) {
  return a.sel === b.sel && a.key === b.key;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值