【手写 Vue2.x 源码】第三十篇 - diff算法-比对优化(上)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一前言

上篇介绍了diff算法-节点比对主要涉及以下几点

  • 介绍了 diff 算法、对比方式、节点复用
  • 实现了外层节点的 diff 算法
  • 不同节点如何做替换更新
  • 相同节点如何做复用更新文本、元素、样式处理

本篇diff算法-比对优化


二比对儿子节点

1前文回顾

上篇通过构建两个虚拟节点来模拟 v-if 的效果通过 patch 方法比对实现了外层节点的复用

let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

执行结果

初始化时为蓝色文本

image.png

更新后变为红色文本

image.png

发现问题

仅更新了外层 div 的 style但 name 并没有更新为 BraveWang
即只做了第一层节点的比对和属性更新没有进行深层的 diff 比对

2如何比对儿子节点

把“新的儿子节点”和“老的儿子节点”都拿出来依次进行比对

//src/vdom/patch.js

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} el    当前真实元素 id#app
 * @param {*} vnode 虚拟节点
 * @returns         新的真实元素
 */
export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;
  if(isRealElement){
    // 1根据虚拟节点创建真实节点
    const elm = createElm(vnode);
    // 2使用真实节点替换掉老节点
    const parentNode = oldVnode.parentNode;
    parentNode.insertBefore(elm, oldVnode.nextSibling); 
    parentNode.removeChild(oldVnode);
    return elm;
  }else{// diff新老虚拟节点比对
    if(!isSameVnode(oldVnode, vnode)){
      return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
    }
    let el = vnode.el = oldVnode.el;
    if(!oldVnode.tag){
      if(oldVnode.text !== vnode.text){
        return el.textContent = vnode.text;
      }else{
        return; 
      }
    }
    updateProperties(vnode, oldVnode.data);

    // TODO:比较儿子节点...
    let oldChildren = oldVnode.children || {};
    let newChildren = vnode.children || {};
  }
}

3新老儿子节点的几种情况

  • 情况 1老的有儿子新的没有儿子
  • 情况 2老的没有儿子新的有儿子
  • 情况 3新老都有儿子

情况 1老的有儿子新的没有儿子

处理方法直接将多余的老 dom 元素删除即可
// src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
    
// 情况 1老的有儿子新的没有儿子直接将多余的老 dom 元素删除即可
if(oldChildren.length > 0 && newChildren.length == 0){
  // 更好的处理由于子节点中可能包含组件需要封装removeChildNodes方法将子节点全部删掉
  el.innerHTML = '';// 暴力写法直接清空
}

备注这里直接清空innerHTML是暴力写法由于子节点中可能包含组件所以更好的处理方式是封装一个 removeChildNodes 方法用于删掉全部子节点

测试方法

let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<div style="color:red"></div>');
let newVnode = render2.call(vm2);

setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

情况 2老的没有儿子新的有儿子

处理方法直接将新的儿子节点放入对应的老节点中即可
//src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};

// 情况 1老的有儿子新的没有儿子直接将多余的老 dom 元素删除即可
if(oldChildren.length > 0 && newChildren.length == 0){
  el.innerHTML = '';
// 情况 2老的没有儿子新的有儿子直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
  newChildren.forEach((child)=>{// 注意这里的child是虚拟节点需要变为真实节点
    let childElm = createElm(child); // 根据新的虚拟节点创建一个真实节点
    el.appendChild(childElm);// 将生成的真实节点放入 dom
  })
}

备注newChildren中的child为虚拟节点需要先通过createElm(child)创建为真实节点

测试

let vm1 = new Vue({
    data() {
        return { name: 'Brave' }
    }
})
let render1 = compileToFunction('<div style="color:blue"></div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);

let vm2 = new Vue({
    data() {
        return { name: 'BraveWang' }
    }
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);

setTimeout(() => {
    patch(oldVnode, newVnode); 
}, 1000);

情况 3新老都有儿子

处理方法进行 diff 比对
// src/vdom/patch.js#patch

...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};

// 情况 1老的有儿子新的没有儿子直接将对于的老 dom 元素干掉即可;
if(oldChildren.length > 0 && newChildren.length == 0){
  el.innerHTML = '';
// 情况 2老的没有儿子新的有儿子直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
  newChildren.forEach((child)=>{
    let childElm = createElm(child);
    el.appendChild(childElm);
  })
// 情况 3新老都有儿子
}else{
  // diff 比对的核心逻辑
  updateChildren(el, oldChildren, newChildren); 
}

这里对“老的有儿子新的没有儿子”和“老的没有儿子新的有儿子”两种特殊情况做了特殊的处理
接下来当新老节点都有儿子时就必须进行 diff 比对了
所以updateChildren 才是 diff 算法的核心


三新老儿子 diff 比对的核心逻辑 updateChildren 方法

1新老儿子 diff 比对方案介绍

继续当新老节点都有儿子时就需要对新老儿子节点进行比对了

新老节点的比对方案是采用头尾双指针的方式进行新老虚拟节点的依次比对

每次节点比对完成如果是头节点就向后移动指针尾节点就向前移动指针

image.png

直至一方遍历完成比对才结束

即“老的头指针和尾指针重合"或"新的头指针和尾指针重合”

image.png

这里为了能够提升diff算法的性能并不会直接全部采用最耗性能的“乱序比对”

而是结合了日常使用场景优先对4种特殊情况进行了特殊的除了头头、尾尾、头尾、尾头

  • 头和头比较将头指针向后移动
  • 尾和尾比较将尾指针向前移动
  • 头和尾比较将头指针向后移动尾指针向前移动
  • 尾和尾比较将尾指针向后移动头指针向前移动

每次比对时优先进行头头、尾尾、头尾、尾头的比对尝试如果都没有命中才会进行乱序比较

2diff 比对的几种特殊情况头头、尾尾、头尾、尾头

备注由于 4 种情况需要画图说明单独一篇第三十一篇 - diff算法-比对优化下

除了这 4 钟特殊情况外就只能进行乱序比对了

虽然是做乱序比对但目标依然是最大程度实现节点复用提升渲染性能

备注乱序比对如何进行节点复用单独一篇第三十二篇 - diff算法-乱序比对

四结尾

本篇diff算法-比对优化上主要涉及以下几个点

  • 介绍了如何进行儿子节点比对
  • 新老儿子节点可能存在的3种情况及代码实现
  • 新老节点都有儿子时的 diff 方案介绍与处理逻辑分析

下篇diff算法-比对优化下

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue