作用

  1. Diff 是一种比较算法,比较两个虚拟 DOM 的区别,也就是比较两个对象的区别;
  2. 只比较平级;
  3. 同一级的变化节点,如果节点相同只是位置交换,则会复用;

整体流程

当数据发生改变的时候,对应的 set 方法会执行,调用数据的 Dep.notify 通知所有的订阅者,订阅者就会通过patch 函数比较,从而给真实的 DOM 打补丁,更新相应的视图;

比对标签

如果标签不一致说明是两个不同元素,在 diff 过程中会先比较标签是否一致;

// patch 关键代码

// 如果标签不一致用新的标签替换掉老的标签
if (oldVnode.tag !== vnode.tag) {
    oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}

// 如果标签一致,有可能都是文本节点,那就比较文本的内容即可
if (!oldVnode.tag) {
    if (oldVnode.text !== vnode.text) {
        oldVnode.el.textContent = vnode.text;
    }
}

比对属性

当标签相同时,可以复用老的标签元素,并且进行属性的比对;

// patch 关键代码
let el = vnode.el = oldVnode.el;
updateProperties(vnode, oldVnode.data);

function updateProperties(vnode, oldProps = {}) {
    let newProps = vnode.data || {};
    let el = vnode.el;
    // 比对样式
    let newStyle = newProps.style || {};
    let oldStyle = oldProps.style || {};
    for (let key in oldStyle) {
        if (!newStyle[key]) {
            el.style[key] = ''
        }
    }
    // 删除多余属性
    for (let key in oldProps) {
        if (!newProps[key]) {
            el.removeAttribute(key);
        }
    }
    for (let key in newProps) {
        if (key === 'style') {
            for (let styleName in newProps.style) {
                el.style[styleName] = newProps.style[styleName];
            }
        } else if (key === 'class') {
            el.className = newProps.class;
        } else {
            el.setAttribute(key, newProps[key]);
        }
    }
}

比对子元素

判断新老节点儿子的状况

var oldChildren = oldVnode.children || [];
var newChildren = vnode.children || [];

if (oldChildren.length > 0 && newChildren.length > 0) {
  // 新老都有儿子 需要比对里面的儿子
  updateChildren(_el, oldChildren, newChildren);
} else if (newChildren.length > 0) {
  // 新的有孩子,老的没孩子,直接将孩子虚拟节点转化成真实节点插入即可
  for (var i = 0; i < newChildren.length; i++) {
    var child = newChildren[i];
    _el.appendChild(createElm(child));
  }
} else if (oldChildren.length > 0) {
  // 老的有孩子,新的没孩子,直接删除老节点的孩子
  _el.innerHTML = '';
}

updateChildren:对比新旧节点都有孩子节点的情况

优化策略-在开头和结尾新增元素(常见)

function isSameVnode(oldVnode, newVnode) {
  // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
  return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}

function updateChildren(parent, oldChildren, newChildren) {
  let oldStartIndex = 0;
  let oldStartVnode = oldChildren[0];
  let oldEndIndex = oldChildren.length - 1;
  let oldEndVnode = oldChildren[oldEndIndex];

  let newStartIndex = 0;
  let newStartVnode = newChildren[0];
  let newEndIndex = newChildren.length - 1;
  let newEndVnode = newChildren[newEndIndex];

  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 优化向后追加逻辑
    if (isSameVnode(oldStartVnode, newStartVnode)) {
      patch(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
      // 优化向前追加逻辑
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      patch(oldEndVnode, newEndVnode); // 比较孩子 
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
    }
  }
  if (newStartIndex <= newEndIndex) {
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      let ele = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
      parent.insertBefore(createElm(newChildren[i]), ele);
    }
  }
}

优化策略-头移尾、尾移头(常见)

// 头移动到尾部
else if (isSameVnode(oldStartVnode, newEndVnode)) {
  patch(oldStartVnode, newEndVnode);
  parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
  oldStartVnode = oldChildren[++oldStartIndex];
  newEndVnode = newChildren[--newEndIndex]
  // 尾部移动到头部
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
  patch(oldEndVnode, newStartVnode);
  parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
  oldEndVnode = oldChildren[--oldEndIndex];
  newStartVnode = newChildren[++newStartIndex]
}

优化策略-暴力比对(乱序对比)

// 对所有的孩子元素进行编号
function makeIndexByKey(children) {
  let map = {};
  children.forEach((item, index) => {
    map[item.key] = index
  });
  return map;
}
let map = makeIndexByKey(oldChildren);

// 用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
  parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
  let moveVnode = oldChildren[moveIndex];
  oldChildren[moveIndex] = undefined;
  parent.insertBefore(moveVnode.el, oldStartVnode.el);
  patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]

// 如果有剩余则直接删除
if (oldStartIndex <= oldEndIndex) {
  for (let i = oldStartIndex; i <= oldEndIndex; i++) {
    let child = oldChildren[i];
    if (child != undefined) { // 在比对过程中,可能出现空值情况则直接跳过
      parent.removeChild(child.el)
    }
  }
}

更新操作

Vue.prototype._update = function (vnode) {
  const vm = this;
  const prevVnode = vm._vnode; // 保留上一次的 vnode
  vm._vnode = vnode;

  if (!prevVnode) {
    vm.$el = patch(vm.$el, vnode); // 通过虚拟节点渲染出真实的 dom,替换掉真实的 $el
  } else {
    vm.$el = patch(prevVnode, vnode); // 更新时做diff操作
  }
}

面试题

什么是虚拟 dom

  1. 虚拟 dom 本质上就是一个普通的 JS 对象,用于描述视图的界面结构;

  2. vue 中,每个组件都有一个 render 函数,每个 render 函数都会返回一个虚拟 dom 树,这也就意味着每个组件都对应一棵虚拟 DOM 树;

为什么需要虚拟 dom

  1. vue 中,渲染视图会调用 render 函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时;如果在渲染时,直接使用真实 DOM ,由于真实 DOM 的创建、更新、插入等操作会带来大量的性能损耗,从而就会极大的降低渲染效率;

  2. 因此 vue 在渲染时,使用虚拟 dom 来替代真实 dom ,主要为解决渲染效率的问题;

虚拟 dom 如何转换为真实 dom

  1. 在一个组件实例首次被渲染时,它先生成虚拟 dom 树,然后根据虚拟 dom 树创建真实 dom ,并把真实 dom 挂载到页面中合适的位置,此时,每个虚拟 dom 便会对应一个真实的 dom

  2. 如果一个组件受响应式数据变化的影响,需要重新渲染时,它仍然会重新调用 render 函数,创建出一个新的虚拟 dom 树,用新树和旧树对比,通过对比,vue 会找到最小更新量,然后更新必要的虚拟 dom 节点,最后,这些更新过的虚拟节点,会去修改它们对应的真实 dom

  3. 这样一来,就保证了对真实 dom 达到最小的改动;

模板和虚拟 dom 的关系

  1. vue 框架中有一个 compile 模块,它主要负责将 模板 转换为 render 函数,而 render 函数调用后将得到虚拟 dom

  2. 编译的过程分两步:

    • 将模板字符串转换成为 AST
    • AST 转换为 render 函数
  3. 如果使用传统的引入方式,则编译时间发生在组件第一次加载时,这称之为运行时编译;

  4. 如果是在 vue-cli 的默认配置下,编译发生在打包时,这称之为模板预编译;

  5. 编译是一个极其耗费性能的操作,预编译可以有效的提高运行时的性能,而且,由于运行的时候已不需要编译,vue-cli 在打包时会排除掉 vue 中的compile 模块,以减少打包体积;

  6. 模板的存在,仅仅是为了让开发人员更加方便的书写界面代码;

  7. vue 最终运行的时候,最终需要的是 render 函数,而不是模板,因此,模板中的各种语法,在虚拟 dom 中都是不存在的,它们都会变成虚拟 dom 的配置

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 作用
  2. 2. 整体流程
  3. 3. 比对标签
  4. 4. 比对属性
  5. 5. 比对子元素
  6. 6. updateChildren:对比新旧节点都有孩子节点的情况
    1. 6.1. 优化策略-在开头和结尾新增元素(常见)
    2. 6.2. 优化策略-头移尾、尾移头(常见)
    3. 6.3. 优化策略-暴力比对(乱序对比)
  7. 7. 更新操作
  8. 8. 面试题
    1. 8.1. 什么是虚拟 dom
    2. 8.2. 为什么需要虚拟 dom
    3. 8.3. 虚拟 dom 如何转换为真实 dom
    4. 8.4. 模板和虚拟 dom 的关系