简单diff算法

  1. 减少DOM操作的性能开销

假设有以下新旧虚拟节点

//oldVNode
const oldVNode = {
  type: "div",
  children: [
    { type: "p", children: "1" },
    { type: "p", children: "2" },
    { type: "p", children: "3" },
  ],
};
//newVNode
const newVNode = {
  type: "div",
  children: [
    { type: "p", children: "4" },
    { type: "p", children: "5" },
    { type: "p", children: "6" },
  ],
};
  • 若卸载全部 DOM之后再重新挂载 DOM ,一共需要6次DOM操作,若只更新 p 标签的文本节点内容,DOM操作次数就可以减半到3次

// 对比新旧vnode的children数组,再调用函数逐个对比新旧vnode的children
function patchChildren(n1, n2, container) {
  if (typeof n2.children === "string") {
    // ...
  } else if (Array.isArray(n2.children)) {
    //实现两组子节点的更新方式
    // 新旧 children
    const oldChildren = n1.children;
    const newChildren = n2.children;
    // 遍历旧的 children
    for (let i = 0; i < oldChildren.length; i++) {
      // 调用patch 函数逐个更新子节点
      patch(oldChildren[i],newChildren[i])
    }
  }else{
    // ...
  }
}
//新旧节点个数不同,则对应挂载、卸载。我们不应该总是遍历旧的子节点,而应该遍历长度较短的那一组
//修改实现如下:(注释下为修改部分)
function patchChildren(n1, n2, container) {
  if (typeof n2.children === "string") {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;
    // 新旧两组子节点的长度
    const oldLen = oldChildren.length;
    const newLen = newChildren.length;
    // 取出较短那一组
    const commonLength = Math.min(oldLen, newLen);
    // 遍历 commonLength 次
    for (let i = 0; i < commonLength; i++) {
      patch(oldChildren[i], newChildren[i], container);
    }
    // 如果 newLen > oldLen 说明新的子节点需要挂载
    // 如果 newLen < oldLen 说明旧的子节点需要卸载
    if (newLen > oldLen) {
      for (let i = commonLength; i < newLen; i++) {
        patch(null, newChildren[i], container);
      }
    } else if (newLen < oldLen) {
      for (let i = commonLength; i < oldLen; i++) {
        unmount(newChildren[i]);
      }
    }
  } else {
    // ...
  }
}
  1. DOM复用与 key 的作用

上一节中代码考虑了两组子节点的数量关系,但是对于仅仅顺序改变的节点diff却完全没有生效

// oldChildren
const oldVNode = {
  type: "div",
  children: [
    { type: "p", children: "1" },
    { type: "div", children: "2" },
    { type: "span", children: "3" },
  ],
};
// newChildren
const newVNode = {
  type: "div",
  children: [
    { type: "div", children: "2" },
    { type: "span", children: "3" },
    { type: "p", children: "1" },
  ],
};
// 调用patch函数在旧子节点 { type: "p", children: "1" },于新子节点  { type: "div", children: "2" },
// 之间补丁,由于两者之间是不同的标签,所以会卸载旧节点挂载新节点,才有上一节的算法会产生6次DOM操作

// 最优的处理方法是通过移动来完成子节点的更新,但是这就需要保证一个前提:新旧两组子节点中存在可复用的节点
// 对子节点移动,为了保证识别子节点,需要引入一个key值来作为唯一标识
// 增加了key属性
const oldVNode = {
  type: "div",
  children: [
    { type: "p", children: "1", key: 1 },
    { type: "div", children: "2", key: 2 },
    { type: "span", children: "3", key: 3 },
  ],
};
const newVNode = {
  type: "div",
  children: [
    { type: "div", children: "222", key: 2 },
    { type: "span", children: "333", key: 3 },
    { type: "p", children: "111", key: 1 },
  ],
};
// 两个虚拟节点拥有相同的key值和 vnode.type 属性值。这意味着可以通过移动来完成更新,但仍需要对这两个节点进行打补丁的操作,因为他们的内容已经改变了。
// 在讨论如何移动DOM之前,我们需要先完成打补丁的操作

function patchChildren(n1, n2, container) {
  if (typeof n2.children === "string") {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;
    const oldLen = oldChildren.length;
    const newLen = newChildren.length;

    // 遍历新的 children
    for (let i = 0; i < newLen; i++) {
      const newVNode = newChildren[i];
      // 遍历旧的 children
      for (let j = 0; j < oldLen; j++) {
        const oldVNode = oldChildren[j];
        // 寻找相同key,若有则可复用,但仍需调用patch函数更新
        if (newVNode.key === oldVNode.key) {
          patch(oldVNode, newVNode, container);
          break;
        }
      }
      patch(oldChildren[i], newChildren[i], container);
    }
  } else {
    // ...
  }
}
// 首次挂载
renderer.render(oldVNode, document.querySelector("#app"));
setTimeout(() => {
  // 1 秒钟后更新
  renderer.render(newVNode, document.querySelector("#app"));
}, 1000);

3. 找到需要移动的元素

  • 第一步:取新子节点的第一个节点p-3,key值为3,在旧数子节点中可以找到相同key值的可复用节点,在旧子节点数组中索引为2

  • 第二步:取p-1,key为1,对应旧节点索引为0,小于2

在这一步,索引的递增顺序被打破:p-1对应的真实DOM需要移动

  • 第三步:取p-2,key为2,对应旧节点索引为1,小于2;p-2对应的真实DOM也需要移动

将p-3在旧children中的索引定义为:在旧children中寻找相同key值节点的过程中,遇到的最大索引值。如果在后续寻找过程中,存在索引值比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动。

使用 lastIndex 变量存储整个寻找过程中遇到的最大索引值

function patchChildren (n1, n2,container) {
  if (typeof n2.children === 'string') {
    // ...
  } 
  else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children
    const newChildren = n2.children
    // 用来存储寻找过程中遇到的最大索引值
    let lastIndex = 0
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i];
      for (let j = 0; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j];
        if (newVNode.key === oldVNode.key) {
          patch(oldVNode, newVNode, container)
          if (j < lastIndex) {
            // 如果当前找到的节点在旧 children 中的索引小于最大索引值 lastIndex
            // 说明该节点对应的真实 DOM 需要移动
          } else {
            // 如果当前找到的节点在旧 children 中的索引不小于最大索引值,则更新 lastIndex 的值
            lastIndex = j
          }
          break;
        }
      }
    }
  } 
  else {
    // ...
  }
}

新节点在旧节点中对应的index比 lastIndex 大,说明该新节点本来就在lastIndex节点下面,不需要移动,否则说明该新节点原本在lastIndex上面的,跑到 lastIndex 下面去了,要移动。

4. 如何移动元素

  1. 当一个虚拟节点被挂载后,其对应的真实 DOM 节点会存储在它的 vnode.el 属性中,因此可以通过旧子节点的 vnode.el 属性取得它对应的真实 DOM 节点。

  1. 当更新操作发生时,渲染器会调用 patchElement 函数在新旧虚拟节点之间进行打补丁。

function  patchElement(n1, n2) {
  // 新的 vnode 也引用了真实 DOM 元素
  const el = n2.el = n1.el
  // ...
}
  1. patchElement 函数首先将旧节点的 n1.el 属性赋值给新节点的 n2.el 属性。这个赋值语句的真正含义是将 DOM 元素的复用:

  1. 接下来实现代码:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值