Vue——DIFF算法

174 阅读4分钟

1.什么是DIFF(比较算法)

  • 用JavaScript对象结果表示DOM树的结构;然后用这个树构建一个真正的DOM树,插入到文档当中。

  • 当状态变更的时候,重新构造一颗新的对象树。然后用新的树和旧的树进行比较(DIFF),记录两棵树差异,把第二棵树所记录的差异应用到第一棵树所构建的真正的DOM树上(patch),视图就更新了。(也就是将旧的VNode树与生成的新VNode树进行比较,比较差异,然后更新DOM(刷新页面),实现这个过程的代码就是DIFF算法

image.png

比较的过程是:

  • 一层一层的比较:父组件与父组件进行比较,并不会把父组件跟子组件比较,同层级时,从两边到中间。

  • 比较的过程中,发现差异(组件/组件类型,文本,属性值,注释等等),就会异步的去给DOM打补丁(操作页面)

注意:同层级时比较,是从两边到中间的。

ED6EC8AEF5B30B4FFB017C00D09633D8.jpg

2.DIFF算法的过程(了解):

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁

  • 通过sameVnode进行判断,相同则调用patchVnoe方法

patch 的核心原理代码

   function patch(oldVnode, newVnode) {
   // 比较是否为一个类型的节点
   if (sameVnode(oldVnode, newVnode)) {
   // 是:继续进行深层比较
   patchVnode(oldVnode, newVnode)
   } else {
   // 否
   const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
   const parentEle = api.parentNode(oldEl) // 获取父节点
    createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
   if (parentEle !== null) {
     api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
     api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
     // 设置null,释放内存
     oldVnode = null
      }
   }

   return newVnode
   }

sameVnode方法判断是否为同一类型节点

function sameVnode(oldVnode, newVnode) {
  return (
      oldVnode.key === newVnode.key && // key值是否一样
      oldVnode.tagName === newVnode.tagName && // 标签名是否一样
      oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
      isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
      sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
    )
}

patchVnode做了以下操作:

  1. 找到对应的真实DOM,称为el

  2. 如果都有文本节点且不相等,将el文本节点设置为Vnode的文本节点

  3. 如果oldVnode有子节点,而VNode没有,则删除el子节点

  4. 如果oldVnode没有子节点,而VNode有,则将VNode的子节点真实化后添加到el

  5. 如果两者都有子节点,则执行updateChildren函数比较子节点

patchVnode方法(当为同一节点时,会调用这个方法进行逐层的对比)

function patchVnode(oldVnode, newVnode) {
  const el = newVnode.el = oldVnode.el // 获取真实DOM对象
  // 获取新旧虚拟节点的子节点数组
  const oldCh = oldVnode.children, newCh = newVnode.children
  // 如果新旧虚拟节点是同一个对象,则终止
  if (oldVnode === newVnode) return
  // 如果新旧虚拟节点是文本节点,且文本不一样
  if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
    // 则直接将真实DOM中文本更新为新虚拟节点的文本
    api.setTextContent(el, newVnode.text)
  } else {
    // 否则

    if (oldCh && newCh && oldCh !== newCh) {
      // 新旧虚拟节点都有子节点,且子节点不一样

      // 对比子节点,并更新
      updateChildren(el, oldCh, newCh)
    } else if (newCh) {
      // 新虚拟节点有子节点,旧虚拟节点没有

      // 创建新虚拟节点的子节点,并更新到真实DOM上去
      createEle(newVnode)
    } else if (oldCh) {
      // 旧虚拟节点有子节点,新虚拟节点没有

      //直接删除真实DOM里对应的子节点
      api.removeChild(el)
    }
  }
}

updateChildren主要做了以下操作:

可以参考:blog.csdn.net/weixin_4500…

  1. 设置新旧VNode的头尾指针

  2. 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用pathchVnode进行patch重复流程,调用createElement创建一个新节点,从哈希表寻找key一致的VNode节点再分情况操作。

updateChildren的核心原理代码


// diff(同层比较)
 function updateChildren (parentElm, oldCh, newCh) {
  // 旧元素开始下标    新元素开始下标
 let oldStartIdx = 0, newStartIdx = 0
// 旧元素末尾开始下下标
 let oldEndIdx = oldCh.length - 1
// 取旧元素开始头节点元素
 let oldStartVnode = oldCh[0]
// 取旧元素末尾节点元素
 let oldEndVnode = oldCh[oldEndIdx]
// 新元素末尾下标
 let newEndIdx = newCh.length - 1
//  取新元素第一个节点元素
 let newStartVnode = newCh[0]
// 取新元素末尾节点元素
 let newEndVnode = newCh[newEndIdx]
// 利用绑定的key值has表
 let oldKeyToIdx
 let idxInOld
 let elmToMove
 let before
 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
   //两两比较
  // 头和头(尾和尾)比较
  // 头和头(尾和尾比较)直接更新替换,  

  //头(旧元素头节点)尾(新元素末尾节点)比较      (old头newOdl尾比较) 条件成立将old头节点元素移动到后面
  //旧节点头节点和新节点的末尾比较 是同一个节点  会将旧节点的第一个节点移动到末尾 

  // 尾(旧元素末尾节点)头(新元素头节点)比较      (newOld尾old头比较) 条件成立将newOld尾节点元素移动到前面
  if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
   oldStartVnode = oldCh[++oldStartIdx] 
  }else if (oldEndVnode == null) {
   oldEndVnode = oldCh[--oldEndIdx]
  }else if (newStartVnode == null) {
   newStartVnode = newCh[++newStartIdx]
  }else if (newEndVnode == null) {
   newEndVnode = newCh[--newEndIdx]
  // 判断旧节点头元素和新节点头元素是否是同一个
  }else if (sameVnode(oldStartVnode, newStartVnode)) {
   patchVnode(oldStartVnode, newStartVnode)
   oldStartVnode = oldCh[++oldStartIdx]
   newStartVnode = newCh[++newStartIdx]
  // 判断旧节尾元素和新节点尾元素是否是同一个
  }else if (sameVnode(oldEndVnode, newEndVnode)) {
   patchVnode(oldEndVnode, newEndVnode)
   oldEndVnode = oldCh[--oldEndIdx]
   newEndVnode = newCh[--newEndIdx]
  //判断旧节点元素的头节点元素和新节点元素末尾元素是否是同一个,条件成立将oldStartVnode移动到后面
  }else if (sameVnode(oldStartVnode, newEndVnode)) {
   patchVnode(oldStartVnode, newEndVnode)
   api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
   oldStartVnode = oldCh[++oldStartIdx]
   newEndVnode = newCh[--newEndIdx]
  // 判断旧节点元素的尾元素和新节点元素的头节点元素是否是同一个,条件成立将oldEndVnode移动到前面
  }else if (sameVnode(oldEndVnode, newStartVnode)) {
   patchVnode(oldEndVnode, newStartVnode)
   api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
   oldEndVnode = oldCh[--oldEndIdx]
   newStartVnode = newCh[++newStartIdx]
  }else {

  // 如果以上两两比较都不成立,会用元素绑定的key生成一个has表
  // 使用key时的比较

   if (oldKeyToIdx === undefined) {
    // 生成has表
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
   }
  // 通过对象的方括号语法可知新元素是否在has表中也存在相同节点,不存在idxInOld变量值为false
   idxInOld = oldKeyToIdx[newStartVnode.key]
  //没有找到的情况
   if (!idxInOld) {
    // 在旧节点元素has表中没有找到,会创新新元素直接插入(例如新节点元素的第一个节点在has表中找不到,就会创建一个新的元素插入到前面)
    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
    newStartVnode = newCh[++newStartIdx]
   }
  //找到的情况  key相同但是元素节点不相同,以新节点元素为准创建新的元素并插入
   else {
    //在has表中存在key相同,但是新节点元素和旧节点元素不是同一个标签元素,创建新的并插入
    elmToMove = oldCh[idxInOld]  
    if (elmToMove.sel !== newStartVnode.sel) {
     api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
    }else {
    //key相同,标签相同情况 
     patchVnode(elmToMove, newStartVnode)
     oldCh[idxInOld] = null
     api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
    }
    // 处理完一个节点元素,新元素节点的下标继续向右(后)移动
    newStartVnode = newCh[++newStartIdx]
   }
  }
 }
//  处理遍历完的情况

// 旧的old所有元素节点遍历完,但是新的newOld未遍历完就会把新节点未遍历完的那部分创建为新的元素并插入到末尾
 if (oldStartIdx > oldEndIdx) {
  before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
  addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
  // 新的newOld遍历完成,但是旧的old未遍历完,就会将old未遍历完的部分删除
 }else if (newStartIdx > newEndIdx) {
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
 }
}