【手写 Vue2.x 源码】第三十三篇 - diff算法-收尾+阶段性总结

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

一前言

上篇diff算法-乱序比对主要涉及以下几个点

  • 介绍了乱序比对的方案
  • 介绍了乱序比对的过程分析
  • 实现了乱序比对的代码逻辑

本篇diff 算法的阶段性梳理


二初渲染与视图更新流程

  • Vue 初渲染时会调用 mountComponent 方法进行挂载在 mountComponent 方法中会创建一个 watcher

  • 当数据更新时进入 Object.defineProperty 的 set 方法在set 方法中会调用 dep.notify() 通知收集的 watcher 调用 update 方法做更新渲染

  • 在 Watcher 类的 update 方法中调用了 queueWatcher 方法将 watcher 进行缓存、去重操作

  • queueWatcher 方法中调用 flushschedulerQueue 方法执行所有 watcher.run 并清空队列

  • Watcher类中的 run 方法内部调用了 Watcher类中的 get 方法记录当前 watcher 并调用 getter

  • this.getter 是 Watcher类实例化时传入的视图更新方法 fn即 updateComponent 视图渲染逻辑

  • 执行 updateComponent 中的 vm._render使用最新数据重新生成虚拟节点并调用 update 更新视图


三diff 算法的外层更新

在 Vue 中每次数据变化时并不会对节点做全量的替换而是会对新老虚拟节点进行 diff 比对

  • 首次渲染根据虚拟节点生成真实节点替换掉原来的节点
  • 更新渲染生成新的虚拟节点并与老的虚拟节点比对复用老节点进行渲染

diff 算法

  • 又叫同层比对算法
  • 深度优先遍历递归
  • 采用了“头尾指针”的处理;
    通过对新老虚拟节点进行比对尽可能复用原有节点以提升渲染性能

节点可复用的依据

  • 标签名和 key 均相同即判定为可复用节点

patch 方法做节点的递归更新通过 oldVnode.nodeType 节点类型判断是否为真实节点

  • 非真实节点即为真实dom时进行初渲染逻辑
  • 是真实节点需要进行新老虚拟节点比对

新老虚拟节点比对

  • 节点不相同时使用新的真实节点createElm(vnode)替换老的真实节点oldVnode.el
    oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
  • 节点相同时复用老节点更新文本、样式等属性即可

文本的处理

  • 文本节点没有标签名
  • 文本节点没有有儿子

元素的处理

  • 新老元素都有的属性用新值覆盖老值
  • 新的没有老的有的属性直接删除掉

style 的处理

  • 老样式对象中有新样式对象中没有删掉多余样式
  • 新样式对象中有覆盖到老样式对象中

四diff 算法的比对优化

1新老儿子节点的情况

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

    处理方法直接将多余的老 dom 元素删除即可

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

    处理方法直接将新的儿子节点放入对应的老节点中即可

  • 情况 3新老都有儿子

    处理方法进行 diff 比对

2新老儿子节点的 diff 比对

  • 新老儿子节点的比对采用了头尾双指针的方法;

  • 新老节点都有儿子时进行头头、尾尾、头尾、尾头对比

  • 头头、尾尾、头尾、尾头均没有命中时进行乱序比对;


五diff 算法的乱序比对

  • 根据老儿子集合创建一个节点 key 和索引 index 的映射关系 mapping用新儿子节点依次到 mapping 中查找是否存在可复用的节点
  • 存在复用节点更新可复用节点属性并移动到对应位置移动走的老位置要做空标记
  • 不存在复用节点创建节点并添加到对应位置
  • 最后再将不可复用的老节点删除

六diff 算法收尾

1问题分析

至此已经完成了 diff 算法的全部逻辑编写但一直使用模拟新老节点更新;

原因在于每次更新时都执行patch(vm.$el, vnode)

// src/lifecycle.js

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 传入当前真实元素vm.$el虚拟节点vnode返回新的真实元素
    vm.$el = patch(vm.$el, vnode);
  }
}

在使用两个虚拟节点模拟 diff 更新时我们已经修改了 patch 方法使之既能够支持初渲染还能支持更新渲染

// src/vdom/patch.js

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} oldVnode  老的虚拟节点
 * @param {*} vnode     新的虚拟节点
 * @returns             新的真实元素
 */
export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;  // 真实节点1虚拟节点无此属性
  if (isRealElement) {// 真实节点
    // 1根据虚拟节点创建真实节点
    const elm = createElm(vnode);
    // 2使用真实节点替换掉老节点
    // 找到元素的父亲节点
    const parentNode = oldVnode.parentNode;
    // 找到老节点的下一个兄弟节点nextSibling 若不存在将返回 null
    const nextSibling = oldVnode.nextSibling;
    // 将新节点 elm 插入到老节点el的下一个兄弟节点 nextSibling 的前面
    // 备注若 nextSibling 为 nullinsertBefore 等价于 appendChild
    parentNode.insertBefore(elm, nextSibling);
    // 删除老节点 el
    parentNode.removeChild(oldVnode);
    
    return elm;
  } else {
    // diff新老虚拟节点比对
    if (!isSameVnode(oldVnode, vnode)) {// 同级比较不是相同节点时不考虑复用放弃跨层复用直接用新的替换旧的
      return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
    }

    // 相同节点就复用节点复用老的再更新不一样的地方属性注意文本要做特殊处理文本是没有标签名的

    // 文本的处理文本直接更新就可以因为文本没有儿子  组件中 Vue.component‘xxx’这就是组件的 tag
    let el = vnode.el = oldVnode.el;  // 节点复用将老节点el赋值给新节点el
    if (!oldVnode.tag) {  // 文本没有标签名
      if (oldVnode.text !== vnode.text) {// 文本内容变化了更新文本内容用新的内容更新老的内容
        return el.textContent = vnode.text;
      }
    }

    // 元素的处理相同节点且新老节点不都是文本时
    updateProperties(vnode, oldVnode.data);

    // 比较儿子节点
    let oldChildren = oldVnode.children || {};
    let newChildren = vnode.children || {};
    // 情况 1老的有儿子新的没有儿子直接把老的 dom 元素干掉即
    if (oldChildren.length > 0 && newChildren.length == 0) {
      el.innerHTML = '';//暴力写法直接清空更好的处理是封装removeChildNodes方法将子节点全部删掉因为子节点可能包含组件
      // 情况 2老的没有儿子新的有儿子直接将新的插入即可
    } else if (oldChildren.length == 0 && newChildren.length > 0) {
      newChildren.forEach((child) => {// 注意这里的child是虚拟节点需要变为真实节点
        let childElm = createElm(child); // 根据新的虚拟节点创建一个真实节点
        el.appendChild(childElm);// 将生成的真实节点放入 dom
      })
      // 情况 3新老都有儿子
    } else {  // 递归: updateChildren 内部调用 patch, patch, 内部还会调用 updateChildren (patch 方法是入口)
      updateChildren(el, oldChildren, newChildren)
    }
    
    return el;// 返回新节点
  }
}

2正常使用方式

将模拟节点更新的代码全部注释掉并修改 index.html

<!-- diff算法 -->
<body>
  <!-- 场景div标签复用仅更新span标签中的文本 name -->
  <div id="app">
    <span>{{name}}</span>
  </div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: "#app",
      data() {
        return { name: 'Brave' }
      }
    });
    setTimeout(() => {
      vm.name = "BraveWang";
    }, 1000);
  </script>
</body>

2测试修改前效果

测试 patch 方法修改前的效果

image.png

测试结果将 div 标签全部干掉重新创建了一次

原因分析每次都执行vm.$el = patch(vm.$el, vnode);没有区分初渲染和更新渲染

3如何区分初渲染和更新渲染

如何区分初渲染和更新渲染

  • 第一次渲染时在 vm.preVnode 上保存当前 Vnode
  • 第二次渲染时先取 vm.preVnode有值就是更新渲染
  • 初渲染执行patch(vm.$el, vnode)
  • 更新渲染执行patch(preVnode, vnode)

4代码实现

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 取上一次的 preVnode
    let preVnode = vm.preVnode;
    // 渲染前先保存当前 vnode
    vm.preVnode = vnode;
    // preVnode 有值说明已经有节点了本次是更新渲染没值就是初渲染
    if(!preVnode){// 初渲染
      // 传入当前真实元素vm.$el虚拟节点vnode返回新的真实元素
      vm.$el = patch(vm.$el, vnode);
    }else{// 更新渲染:新老虚拟节点做 diff 比对
      vm.$el = patch(preVnode, vnode);
    }
  }
}

5测试修改后的效果

测试 patch 方法修改后的效果

image.png

测试结果div 标签被复用只更新了 span 中的name


七结尾

本篇diff算法阶段性梳理主要涉及以下几个点

  • 初渲染与视图更新流程
  • diff 算法的外层更新
  • diff 算法的比对优化
  • diff 算法的乱序比对
  • 初渲染和更新渲染判断

下篇组件的初始化流程介绍


更新日志

20210807添加“diff 算法收尾”部分更新“结尾”部分更新文章标题和摘要

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

“【手写 Vue2.x 源码】第三十三篇 - diff算法-收尾+阶段性总结” 的相关文章