react如何diff新老VDOM(不带key)

708 阅读3分钟

思路

只更新发生变化的节点,可以diff新老VDOM

diff流程

  • 由于VDOM是tag/props/children三个属性的组合,可以根据节点类型变化/属性变化/子节点变化来定义diff产物的数据结构
// 定义节点变化类型的枚举
const NodePatchType = {
  CREATE: 'CREATE', // 创建
  DELETE: 'DELETE', // 删除
  UPDATE: 'UPDATE', // 更新
  REPLACE: 'REPLACE' // 替换
}
// 定义节点属性变化类型的么句
const PropsPatchType = {
  DELETE: 'DELETE', // 删除
  UPDATE: 'UPDATE' // 更新
}

// diff产物结构
{
    type: NodePatchType, // 节点变化类型
    props: [PropsPatchType], // 属性变化类型
    children: [
        {
            type: NodePatchType, // 子节点变化类型
            props: [PropsPatchType], // 子节点属性变化类型
            children: []
        }
    ]
}
  • 节点类型变化的diff,由枚举值可以看出,存在增删改替换四种情况。(新老节点没有都不存在的情况)
    • 新增:diff的老节点不存在,且存在新节点时,需要新建一个节点
    • 删除:diff的老节点存在,且新节点不存在时,需要删除节点
    • 替换:新老节点都存在时,节点的类型不一致需要替换。当类型相同时,如果是文本类型,需要判断文本是否一样, 不一样需要替换;如果不是文本类型,需要判断节点的标签是否一样,不一样需要替换
    • 更改:现在还剩下新老节点都是一样的文本和新老节点不是文本但标签相同的情况。当文本一致不需要操作,标签一致需要进一步判断标签属性和子节点是否一致,不一致需要更改节点
    function diffDom(oldVDom, newVDom) {
      // 新增
      if (!oldVDom) {
        return {
          type: NodePatchType.CREATE,
          dom: newVDom
        }
      }
      // 删除
      if (!newVDom) {
        return {
          type: NodePatchType.DELETE
        }
      }
      /**
       * 替换 满足条件
       * 1. 新老dom类型不同
       * 2. 类型相同时,当是字符串或者数字需要判断内容是否相同
       * 3. 类型相同时,且不是字符串或者数字,判断节点的tag是否相同
       */
      if (typeof oldVDom !== typeof newVDom ||
        ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||
        oldVDom.tag !== newVDom.tag
      ) {
        return {
          type: NodePatchType.REPLACE,
          dom: newVDom
        }
      }
      // 更新
      if (oldVDom.tag) {
        const propsPatch = diffProps(oldVDom.props, newVDom.props);
        let childrenPatch = [];
        // diff子节点
        const childrenLenth = Math.max(oldVDom.children.length, newVDom.children.length);
        for (let i = 0; i < childrenLenth; i++) {
          childrenPatch.push(diffDom(oldVDom.children[i], newVDom.children[i]));
        }
        if (propsPatch.length > 0 || childrenPatch.length > 0) {
          return {
            type: NodePatchType.UPDATE,
            props: propsPatch,
            children: childrenPatch
          }
        }
      }
      return null;
    }
    
  • 属性:可以将新老属性合并在一块,遍历属性的集合判断是否存在该属性来diff。属性是否存在删改两种case
    • 删除:当集合中某个属性不在新属性中,需要删除该属性
    • 更改:集合中的某个属性,在新老属性中的值不一样,需要更改该属性
    function diffProps(oldProps, newProps) {
      const propsPatch = [];
      const allProps = {...oldProps, ...newProps};
      Object.keys(allProps).forEach((p) => {
        if (!newProps[p]) {
          propsPatch.push({
            type: PropsPatchType.DELETE,
            props: {
              key: p
            }
          })
        }
        if (oldProps[p] !== newProps[p]) {
          propsPatch.push({
            type: PropsPatchType.UPDATE,
            props: {
              key: p,
              value: newProps[p]
            }
          })
        }
      })
      return propsPatch;
    }
    
  • children:children的diff其实和父节点的diff方法一样,递归处理。不过这里需要考虑一种情况,新老children的子节点数不一样,遍历children的时候需要取子节点最多的那个。
    function diffChildren(oldChildren, newChildren) {
        let childrenPatch = [];
        const childrenLenth = Math.max(oldChildren.length, newChildren.length);
        for (let i = 0; i < childrenLenth; i++) {
            childrenPatch.push(diffDom(oldChildren[i], newChildrenen[i]));
        }
        return childrenPatch;
    }
    

更新真实DOM

更新流程:根节点的变化开始更新 -> 属性变化更新 -> 子节点变化更新

// 更新节点属性
function updateElementProps(dom, props) {
  props.forEach(p => {
    const { props: { key, value } } = p;
    if (p.type = PropsPatchType.DELETE) {
      dom.removeAttribute(key);
    }
    if (p.type = PropsPatchType.UPDATE) {
      dom.setAttribute(key, value);
    }
  })
}
// 更新真实DOM,由于要获取当前节点进行删改替换,element必须是父节点
function updateElement(element, diffPatch, index = 0) {
  if (!diffPatch) {
    return;
  }

  // 新增节点
  if (diffPatch.type === NodePatchType.CREATE) {
    element.appendChild(createElement(diffPatch.dom));
  }
  // 获取当前节点,以便后续删除替换和更新
  const curDom = element.childNodes[index];
  // 删除当前节点
  if (diffPatch.type === NodePatchType.DELETE) {
    element.removeChild(curDom)
  }
  // 替换当前节点
  if (diffPatch.type === NodePatchType.REPLACE) {
    element.replaceChild(createElement(diffPatch.dom), curDom);
  }
  // 更新当前节点
  if (diffPatch.type === NodePatchType.UPDATE) {
    updateElementProps(curDom, diffPatch.props);
    // 更新子节点
    diffPatch.children.forEach((c, i) => {
      updateElement(curDom, c, i);
    });
  }
}

主流程

// 获取新vdom
const newVDom = View();
// 得到diff产物
const diffPatch = diffDom(oldVDom, newVDom);
// 更新DOM
updateElement(element, diffPatch);

以上实操代码可以在git上找到

参考文章

你不知道的Virtual DOM(二):Virtual Dom的更新