阅读 102

vue的dom-diff算法

前言

dom-diff算法是为了在更新dom的时候尽可能地减少dom操作,本文主要阐述vue2.x中的dom-diff算法,它的主要原理就是比对新老两个虚拟节点。其中虚拟节点的格式如下:

function vnode(tag,data,key,children,text{
  return {
    tag,
    data,
    key,
    children,
    text
  }
}
复制代码

两个函数

在讲述dom-diff前先说明两个函数,分别是根据虚拟节点创建真实节点和更新节点属性。

  • 根据虚拟节点创建真实节点
// 创建真实dom
export function createElm(vnode{
  let {tag,children,key,data,text} = vnode;
  if(typeof tag == 'string'){ // 创建元素 放到vnode.el上
    vnode.el = document.createElement(tag);
    // 更新属性
    updateProperties(vnode);
    children.forEach(child => { // 遍历儿子 将子节点渲染后的结果添加到父节点中
      vnode.el.appendChild(createElm(child));
    });
  }else { // 创建文件,放到vnode.el上
    vnode.el = document.createTextNode(text);
  }
  return vnode.el;
}
复制代码
  • 根据虚拟节点更新节点属性
/ 更新属性
function updateProperties(vnode,oldProps={}{
  let newProps = vnode.data || {};// 新属性
  let el = vnode.el;
  // 老的有 新的没有  需要删除
  for (let key in oldProps) {
    if (!newProps[key]) {
      el.removeAttribute(key); // 移除真实dom属性
    }
  }
  // 样式处理 老的 style={color:red} 新的 style={background:red}
  let newStyle = newProps.style || {};
  let oldStyle = oldProps.style || {};
  // 老的样式中 新的没有  删除老的样式
  for (let key in oldStyle) {
    if (!newStyle[key]) {
      el.style[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])
    }
  }
}
复制代码

新旧节点比对

在vue中新旧虚拟节点比对的函数是patch,其比对过程如下:

export function patch(oldVnode,vnode{
  // 默认初始化时 是直接用虚拟节点创建出真实节点 替换老节点
  if (oldVnode.nodeType === 1) { // 真实节点
    // 将虚拟节点转化成真实节点
    let el = createElm(vnode); // 产生真实的dom
    let parentElm = oldVnode.parentNode; // 获取老的app的父亲-》 body
    parentElm.insertBefore(el,oldVnode.nextSibling); // 当前真实元素的后面  如果直接appendChild会导致插入在script脚本后面
    parentElm.removeChild(oldVnode); // 删除老的节点
    return el;
  }else{
    // 在更新时 进行新虚拟节点和老虚拟节点做对比 将不同的地方更新真实的DOM
    // 1.比较两个元素的标签,标签不一样直接替换即可
    if (oldVnode.tag !== vnode.tag) {
      return oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
    }
    // 2.标签可能一样 
    // 都是文本标签 tag都是undefined
    if (!oldVnode.tag) { // 文本比对
      if(oldVnode.text !== vnode.text){
        return oldVnode.el.textContent = vnode.text
      }
    }
    // 3.标签一样  并且需要开始比对标签的属性和儿子
    // 标签一样直接复用
    let el = vnode.el = oldVnode.el
    // 更新属性  用新的虚拟节点比对老的,更新节点
    updateProperties(vnode,oldVnode.data);// 新老属性做对比
    
    let oldChildren = oldVnode.children || [];
    let newChildren = vnode.children || [];
    // 子元素比对  分为以下几种情况:
    if (oldChildren.length>0 && newChildren.length>0) {
      // 老的有儿子  新的也有儿子
      updateChildren(oldChildren,newChildren,el)
    } else if(oldChildren.length>0){
      // 老的有儿子  新的没有儿子
      el.innerHTML=""
    } else if (newChildren.length>0) { 
      // 老的没儿子  新的有儿子
      for (let i = 0; i < newChildren.length; i++) {
        let child = newChildren[i];
        el.appendChild(createElm(child))
      }
    }
  }
}
复制代码

updateChildren函数

updateChildren函数是dom-diff算法的核心,具体实现逻辑如下:

function isSameVnode(oldVnode,newVnode{
  return (oldVnode.tag == newVnode.tag) && (oldVnode.key == newVnode.key)
}
// vue中的diff算法
function updateChildren(oldChildren,newChildren,parent{
  // DOM中常见的操作:把节点插入到当前儿子的头部、尾部、儿子倒序正序
  // vue2中采用的是双指针
  // 做一个循环 同时循环新老虚拟节点 哪个先节点 循环就停止
  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];
  function makeIndexByKey(children{
    let map = {}
    children.forEach((item,index)=>{
      if (item.key) {
        map[item.key] = index
      }
    })
    return map;
  }
  let map = makeIndexByKey(oldChildren);
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (!oldStartVnode) { // 指针指向null 跳过这次处理
      oldStartVnode = oldChildren[++oldStartIndex]
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex]
    }else 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]
    }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]
    }else {
      // 儿子之间没有关系   暴力比对
      let moveIndex = map[newStartVnode.key]; // 新虚拟节点开头的值
      if (moveIndex == undefined) { // 不需要移动 说明没有复用的key
        parent.insertBefore(createElm(newStartVnode),oldStartVnode.el)
      } else {
        let moveVnode = oldChildren[moveIndex]; // 对应老的虚拟节点
        oldChildren[moveIndex] = null;
        parent.insertBefore(moveVnode.el,oldStartVnode.el);
        patch(moveVnode,newStartVnode); // 比较属性和儿子
      }
      newStartVnode = newChildren[++newStartIndex]
    }
  }
  if (newStartIndex <= newEndIndex) {
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      // parent.appendChild(createElm(newChildren[i]));
      let ele = newChildren[newEndIndex+1] == null ? null : newChildren[newEndIndex+1].el
      // 向后插入 ele = null
      // 向前插入 ele 就是当前元素前面插入
      parent.insertBefore(createElm(newChildren[i]),ele)
    }
  }
  // 老节点未处理的都删除掉,如果是null,跳过即可
  if (oldStartIndex <= oldEndIndex) {
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      let child = oldChildren[i];
      if (child !== null) {
        parent.removeChild(child);
      }
    }
  }
}
复制代码

总结

vue中的DOM-DIFF算法主要分为两个部分:patch和updateChildren。两个过程的处理逻辑如下图所示。

文章分类
前端
文章标签