vue2源码学习--09dom更新diff算法

diff算法属于是老生常谈了,也是趁着这次手写源码,把这套流程实实在在的写一遍。

之前写render函数的时候已经实现了部分patch方法,patch当时就说了有第一次从虚拟dom生成真实dom的功能,也有两个虚拟dom做比较更新的功能,前者我们已经实现,这次主要是实心dom更新。

为方便我们测试 我们先随便在一个文件写下两个dom的更新操作,我这里写在index.js里

js 复制代码
// compileToFunction就是render函数会生成虚拟dom
let render1 = compileToFunction(`<ul key="1" style="color:red">
    {{name}}
</ul>`)
let vm1 = new Vue({data:{name:'hk'}})
let preVnode = render1.call(vm1)

// 生成真实dom
let el = createElm(preVnode) 
document.body.appendChild(el)

let render2 = compileToFunction(`<ul key="1" style="color:red; background:blue">
    {{name}}
</ul>`)
let vm2 = new Vue({data:{name:'hk'}})
let nextVnode = render2.call(vm2)

// 如果没有diff算法就是直接替换 类似于下边
//setTimeout(() => {
//    let newEl = createElm(nextVnode)
//    el.parentNode.replaceChild(newEl,el)
//},1000)

// 上边提到patch有更新的功能 所以我们应该是用patch方法去完成dom比对和更新
patch(preVnode, nextVnode)

以下我们去完善patch方法

思路:

  1. 判断是不是相同节点(注意diff算法永远是同级比较),通过tag和key来比较,非相同节点,直接删除老节点换新的
  2. 如果是相同节点,复用旧节点,判断是不是文本,如果是文本,判断新文本和旧文本是否一致,不一致新文本赋值给就文本
  3. 对比属性,这要分style和其他属性,但做的操作是一样的就是,要循环遍历旧属性,看新节点上有没有没有的话删掉该属性,然后循环遍历新的属性直接赋值(新的覆盖老的)
  4. 对比子节点,首先判断新旧是不是都有子节点,如果都有子节点进行核心对比优化算法(下边单独说),如果只有新的有把新的挂到旧的上,如果只有旧的有,就把旧的删除
js 复制代码
export function patchProps(el, oldProps = {} ,props = {}) { 
    // 老的属性中有 新的没有 要删除老的
    let oldStyles = oldProps.style || {};
    let newStyle = props.style || {};
    for (const key in oldStyles) {
        if(!newStyle[key]) {
            el.style[key]=''
        }
    }
    for (const key in oldProps) {
        if(!props[key]) {
            el.removeAttribute(key)
        }
    }
    // 用新的覆盖老的
    for (let key in props) {
       if(key === 'style') {
            for (const styleName in props.style) {
                el.style[styleName] = props.style[styleName]
            }
       } else {
            el.setAttribute(key, props[key])
       }
    }
}
export function patch(oldVnode, vnode) {
  // 初渲染流程
  const isRealElement = oldVnode.nodeType // nodeType 原生方法
  if(isRealElement) {
      const elm = oldVnode //真实元素
      const parentElm = elm.parentNode //父元素
      let newEle = createElm(vnode)
      parentElm.insertBefore(newEle, elm.nextSibling)
      parentElm.removeChild(elm)
      return newEle
  } else {
      patchVnode(oldVnode, vnode)
  }
}

function patchVnode(oldVnode, vnode) {
  // diff 算法
 // 1、非相同节点 直接删除老的换新的 没有对比
 // 2、两个节点是同一节点(判断节点的tag和key)比较两个节点的属性是否有差异 
 // 3、节点比较完毕后需要比较两个儿子
 
 if(!isSameVnode(oldVnode, vnode)) { // tag === tag key === key
     let el = createElm(vnode)
     oldVnode.el.parentNode.replaceChild(el, oldVnode.el)
     return el
 }
 // 文本的情况 文本我们期望比较内容
 let el = vnode.el = oldVnode.el
 if(!oldVnode.tag) { // 是文本
     if(oldVnode.text !== vnode.text) {
         el.textContent = vnode.text
     }
 }
 // 是标签 比对属性
 patchProps(el, oldVnode.data, vnode.data)

 // 比较子节点 一方有儿子一方没儿子
 // 两方都有儿子

 let oldChildren = oldVnode.children || []
 let newChildren = vnode.children || []

 if(oldChildren.length>0 && newChildren.length >0) {
     // 完整diff算法
     updateChildren(el, oldChildren, newChildren)
     
 }else if(newChildren.length > 0) { // 没有老的,新的
     mountChildren(el, newChildren)
 }else if(oldChildren.length>0) { // 新的没有 老的有 删除
     // unmountChildren(el, oldChildren)
     el.innerHTML = ''
 }
 
 return el
}
function mountChildren(el, newChildren) {
  for (let i = 0; i < newChildren.length; i++) {
      let child = newChildren[i]
      el.appendChild(createElm(child))
  }
}

新旧都有子节点会调用updateChildren去比对子节点。 这里其实就是对数组常见的操作方法进行优化例如:push,pop,shift,unshift,sort,reverse, 而vue比对方法,采用了双指针的方式进行循环比对(新头,旧头,新尾,旧尾)新或旧头大于尾则循环停止

循环结束 如果新头还是小于等于新尾则,将新的剩余节点插入,因为有可能是尾指针向前导致跳出循环,所以要判断新尾节点后还有没有节点有则插入到后一个节点之前。

如果是旧头小于等于旧尾则删除掉尾部节点

  1. 新头跟旧头比,相同则递归比较两个的子节点,同时新头旧头+1
  2. 新尾和旧尾对比,相同则递归比较子节点,同时新尾旧尾-1
  3. 交叉比对,应对 abcd => dabc这种情况旧尾和新头对比,相同递归比较子节点,将旧尾插入到旧头之前,同时新头+1 旧尾-1
  4. 交叉比对,应对 abcd => bcda 旧头和新尾进行对比,相同则递归比较子节点,将旧头插入到旧尾的下一个之前,同时旧头+1 新尾-1
  5. 乱序比对,仍然是为了最大限度的复用原节点,所以对原节点做了一个map,key就是原节点的key,value是他的index,新的从第一个开始从map上找如果能找到,就把旧节点对应下标的节点插入到就头前,同时将原位置标记为undefind不直接删除防止数组塌陷,然后递归对比子节点,新头后移。如果找不到则创建一个节点插入到旧头之前。
js 复制代码
function updateChildren(el, oldChildren, newChildren) {
  // 我们为了比较两个儿子的时候, 提升性能 有些优化手段
  // vue2 中通过双指针方式进行比较
  let oldStartIndex = 0;
  let newStartIndex = 0;
  let oldEndIndex = oldChildren.length - 1;
  let newEndIndex = newChildren.length - 1;

  let oldStartVnode = oldChildren[0];
  let newStartVnode = newChildren[0];

  let oldEndVnode = oldChildren[oldEndIndex];
  let newEndVnode = newChildren[newEndIndex];

  function makeIndexByKey(children) {
    let map = {};
    children.forEach((child, index) => {
      map[child.key] = index;
    });
    return map;
  }
  let map = makeIndexByKey(oldChildren);
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIndex];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex];
    // 双方有一方头指针大于尾指针 则停止循环

    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 比较开头节点
      patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 比较结尾节点
      patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      // 交叉比对 abcd dabc
      patchVnode(oldEndVnode, newStartVnode);
      el.insertBefore(oldEndVnode.el, oldStartVnode.el);
      newStartVnode = newChildren[++newStartIndex];
      oldEndVnode = oldChildren[--oldEndIndex];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      // 交叉比对 abcd dabc
      patchVnode(oldStartVnode, newEndVnode);
      el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      newEndVnode = newChildren[--newEndIndex];
      oldStartVnode = oldChildren[++oldStartIndex];
    } else {
      // 乱序比对
      // 根据老的列表做一个映射关系,用新的去找,找到则移动,找不到则添加,最后多余的就删除
      let moveIndex = map[newStartVnode.key]; //如果拿到则说明是要移动的索引
      if (moveIndex !== undefined) {
        let moveVnode = oldChildren[moveIndex];
        el.insertBefore(moveVnode.el, oldStartVnode.el);
        oldChildren[moveIndex] = undefined; // 标识这个子节点已经移走了
        patchVnode(moveVnode, newStartVnode);
      } else {
        el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
      }
      newStartVnode = newChildren[++newStartIndex];
    }
  }
  if (newStartIndex <= newEndIndex) {
    // 多余一个插入进去
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      let childEl = createElm(newChildren[i]);
      // 可能是向后追加 也可能是向前追加
      // el.appendChild(childEl)
      let anchor = newChildren[newEndIndex + 1]
        ? newChildren[newEndIndex + 1].el
        : null;
      el.insertBefore(childEl, anchor);
    }
  }
  if (oldStartIndex <= oldEndIndex) {
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      if (oldChildren[i]) {
        let childEl = oldChildren[i].el;
        el.removeChild(childEl);
      }
    }
  }
}

看下效果

不会弄屏幕动图,不太好演示,实际上已经达到效果,无论是向后追加,向前追加,交叉比对,乱序比对,都最大程度复用老节点,实现diff算法。

相关推荐
前端郭德纲3 分钟前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解9 分钟前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里9 分钟前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱10 分钟前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster28 分钟前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw30 分钟前
UniAPP快速入门教程(一)
前端·uni-app
nameofworld40 分钟前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
帅比九日1 小时前
【HarmonyOS NEXT】实战——登录页面
前端·学习·华为·harmonyos
摇光931 小时前
promise
前端·面试·promise
麻花20132 小时前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习