简单diff算法

2023/7/14 vue

# 减少 DOM 操作的性能开销

1.默认处理

旧的全部卸载,然后再将新的一组子节点全部挂载到容器中

// 代码运行到这里,则说明旧子节点都是一组节点,这里涉及及核心的Diff算法
// 将旧的一组子节点全部卸载
n1.children.forEach((c) => unmount(c));
// 再将新的一组子节点全部挂载到容器中
n2.children.forEach((c) => patch(null, c, container));

2.减少操作

  • 取两个数组的最小长度,最小长度部分新的节点替换旧的节点
  • 如果新数组的长度大于旧数组长度说明有新增,直接挂载到后面
  • 如果新数组小于旧的数组长度说明有删除,把多余最小长度的部分给卸载掉
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 旧的一组子节点的长度
const oldLen = oldChildren.length;
// 新的一组子节点的长度
const newLen = newChildren.length;
// 两组子节点的公共长度,即两者中较短的那一组子节点的长度
const commonLength = Math.min(oldLen, newLen);
// 遍历commonLength 次
// 遍历children
for (let i = 0; i < commonLength; i++) {
  // 调用patch 函数逐个更新子节点
  patch(oldChildren[i], newChildren[i], container);
}
// 如果 newLen > oldLen,说明有新子节点需要挂载
if (newLen > oldLen) {
  for (let i = commonLength; i < newLen; i++) {
    patch(null, newChildren[i], container);
  }
} else if (oldLen > newLen) {
  // 如果oldLen > newLen, 说明有旧的子节点需要卸载
  for (let i = commonLength; i < oldLen; i++) {
    unmount(oldChildren[i]);
  }
}

# DOM 复用与 key 的作用

const oldVnode = {
  type: "div",
  children: [
    { type: "p", children: "1", key: 1 },
    { type: "p", children: "2", key: 2 },
    { type: "p", children: "hello", key: 3 },
  ],
};

const newVnode = {
  type: "div",
  children: [
    { type: "p", children: "world", key: 3 },
    { type: "p", children: "1", key: 1 },
    { type: "p", children: "2", key: 2 },
  ],
};

key 处理

遍历新的数组和旧数组中的 key 进行比较,如果 key 相同则更新

// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
  const newVnode = newChildren[i];
  let j = 0;
  // 遍历旧的children
  for (j; j < oldChildren.length; j++) {
    const oldVnode = oldChildren[j];
    // 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
    if (newVnode.key === oldVnode.key) {
      patch(oldVnode, newVnode, container);
      break;
    }
  }
}

修改值后

const vnode = {
  type: "div",
  children: [
    { type: "p", children: "1", key: 1 },
    { type: "p", children: "2", key: 2 },
    { type: "p", children: "world", key: 3 },
  ],
};

修改后虚拟 DOM 顺序不正确,需要找到要移动的元素

# 找到需要移动的元素

















 
 
 
 
 
 
 
 
 






// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;

// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
  const newVnode = newChildren[i];
  // 遍历旧的children
  for (let j = 0; j < oldChildren.length; j++) {
    const oldVnode = oldChildren[j];
    // 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
    if (newVnode.key === oldVnode.key) {
      patch(oldVnode, newVnode, container);
      if (j < lastIndex) {
        // 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
        // 说明该节点对应的真实 DOM 需要移动
      } else {
        // 如果当前找到的节点在旧 children 中的索引不小于最大索引值,

        // 则更新 lastIndex 的值
        lastIndex = j;
      }
      console.log(lastIndex);
      break;
    }
  }
}

# 移动元素

















 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 





// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;

// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
  const newVnode = newChildren[i];
  // 遍历旧的children
  for (let j = 0; j < oldChildren.length; j++) {
    const oldVnode = oldChildren[j];
    // 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
    if (newVnode.key === oldVnode.key) {
      patch(oldVnode, newVnode, container);
      if (j < lastIndex) {
        // 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
        // 说明该节点对应的真实 DOM 需要移动
        const preVNode = newChildren[i - 1];
        if (preVNode) {
          // 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
          // 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
          const anchor = preVNode.el.nextSibling;
          // 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
          // 也就是 prevVNode 对应真实 DOM 的后面
          insert(newVnode.el, container, anchor);
        }
      } else {
        // 如果当前找到的节点在旧 children 中的索引不小于最大索引值,
        // 则更新 lastIndex 的值
        lastIndex = j;
      }
      break;
    }
  }
}

# 添加新元素














 





 

























 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;

// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
  const newVnode = newChildren[i];
  // 遍历旧的children
  // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
  // 初始值为 false,代表没找到
  let find = false;
  for (let j = 0; j < oldChildren.length; j++) {
    const oldVnode = oldChildren[j];
    // 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
    if (newVnode.key === oldVnode.key) {
      // 一旦找到可复用的节点,则将变量 find 的值设为 true
      find = true;
      patch(oldVnode, newVnode, container);
      if (j < lastIndex) {
        // 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
        // 说明该节点对应的真实 DOM 需要移动
        const preVNode = newChildren[i - 1];
        if (preVNode) {
          // 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
          // 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
          const anchor = preVNode.el.nextSibling;
          // 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
          // 也就是 prevVNode 对应真实 DOM 的后面
          insert(newVnode.el, container, anchor);
        }
      } else {
        // 如果当前找到的节点在旧 children 中的索引不小于最大索引值,

        // 则更新 lastIndex 的值
        lastIndex = j;
      }
      break;
    }
  }
  // 如果代码运行到这里,find 仍然为 false,
  // 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点
  // 也就是说,当前 newVNode 是新增节点,需要挂载
  if (!find) {
    // 为了将节点挂载到正确位置,我们需要先获取锚点元素
    // 首先获取当前 newVNode 的前一个 vnode 节点
    const prevVNode = newChildren[i - 1];
    let anchor = null;
    if (prevVNode) {
      // 如果有前一个 vnode 节点,则使用它的下一个兄弟节点作为锚点元素
      anchor = prevVNode.el.nextSibling;
    } else {
      // 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节
      // 这时我们使用容器元素的 firstChild 作为锚点
      anchor = container.firstChild;
    }
    // 挂载 newVNode
    patch(null, newVnode, container, anchor);
  }
}

# 移除不存在的元素






















 
 
 
 
 
 
 
 
 
 


// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;

// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
  const newVnode = newChildren[i];
  // 遍历旧的children
  // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
  // 初始值为 false,代表没找到
  let find = false;
  for (let j = 0; j < oldChildren.length; j++) {
    // 省略...
  }
  if (!find) {
    // 省略...
  }

  // 遍历旧的节点
  for (let i = 0; i < oldChildren.length; i++) {
    const oldVNode = oldChildren[i];
    // 拿旧子节点 oldVNode 去新的一组子节点中寻找具有相同 key 值的节点
    const has = newChildren.find((vnode) => vnode.key === oldVNode.key);
    if (!has) {
      // 如果没有找到相同的节点,则移除
      unmount(oldVNode);
    }
  }
}