深入学习Vue.js(六)简单Diff算法

1.Diff函数的基本思路

   简单来说,当新旧vonde的子节点都是一组节点,为了以最小的性能开销来完成更新操作,需要比较两组子节点,用于比较的算法就叫做Dff算法。

   首先,以最基本的更新两组子节点的方法为例,通常我们会先将旧节点全部卸载,再安装所有的新节点,那么将会执行旧节点数量+新节点数量次。但有时新节点和旧节点之间,节点类型是想相同的,只有节点内容不同,这是我们可以直接修改节点内容,而省略一次挂载操作。这样就直接将执行时间缩短为之前的一半了。

function patchChildren(n1, n2, container) {
    if (typeof n2.children === "string") {
      // ...
    } else if (Array.isArray(n2.children)) {
      if (Array.isArray(n1.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;
        for (let i = 0; i < oldChildren.length; i++) {
          patch(oldChildren[i], newChildren[i]); // patch函数会比较两个节点之间的差异
        }
      } else {
        // ...
      }
  }

   当然,上述代码的问题也很明显————只有新旧DOM的chidren一样多的时候,以上代码才能正常工作。当新一组节点的长度大于旧节点时吗,将会有新节点被直接挂载,而当旧一组节点的长度大于新节点是,将会有节点被直接卸载。

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.max(oldLen, newLen);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i]);
        }
        // 如果旧节点比较长,直接卸载长出来的哪一部分
        if (oldLen > newLen) {
          for (let i = commonLength; i < oldLen; i++) {
            unmount(oldChildren[i]);
          }
        } else if (newLen > oldLen) {
          // 如果新节点比较长,直接挂载长出来哪一部分
          for (let i = commonLength; i < newLen; i++) {
            patch(null, newChildren[i], container);
          }
        }
    }
  }

   这样渲染器就能无视数量差异去渲染它们了。

2.DOM复用和key的作用

   在上节中,通过减少DOM操作降低了性能消耗,但是节点之间的比较只是最普通的顺序比较,对于存在重复节点的情况并没有考虑,比如说,对于下面两个结点:

const oldNode = {
    type: 'div',
    children: [
        {type: 'p'},
        {type: 'span'}
    ],
}
const newNode = {
    type: 'div',
    children: [
        {type: 'span'},
        {type: 'p'}
    ],
}

   如果使用上述代码执行,将会产生四次操作。但实际上,只需要一次交换操作就行了,以为oldNode和newNode之间只是结点的顺序不一样而已。而为了确定新老Node之间是否存在相同结点,就需要引入一个key作为标记,只要两个节点的type和key属性是一样的,那么我们就认为它们是相同节点,但是我们仍然还是要对两个节点进行打补丁的操作,因为节点的内容可能发生了变化。

const oldNode = {
    type: 'div',
    children: [
        {type: 'p', key: 1 },
        {type: 'span', key:2 }
    ],
}
const newNode = {
    type: 'div',
    children: [
        {type: 'span', key: 2 },
        {type: 'p', key: 1 }
    ],
}

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

        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);
              break;
            }
          }
        }
    }
  }

3.标记需要移动的元素

   前面我们通过key完成了元素的更新,但是元素的移动还没有实现。这里我们可以用索引的顺序,来判断节点是否需要移动。这里我们使用一个lastIndex变量来存储当前遍历过的最大索引,如果当前遍历的元素在旧children中的索引小于当前最大索引,那么就说明该元素是需要移动的。

function patchChildren(n1, n2, container) {
    if (typeof n2.children === "string") {
      // ...
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.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) {
                // 说明该节点需要移动
              } else {
                // 否则,更新lastIndex
                lastIndex = j;
              }
              break;
            }
          }
        }
    }
  }

4.移动元素

   标记完需要移动的元素之后,要做的就是移动真实DOM了,这里我们通过vnode获取到它所对应的真实DOM。而当前vnode所在位置,实际上就是在新children中的位置,我们只需要将它插入到当前newChildren节点队列里就行了。

function patchChildren(n1, n2, container) {
    if (typeof n2.children === "string") {
      // ...
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.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) {
                // 说明该节点需要移动
                const preNode = newChildren[i - 1];
                if (preNode) {
                  // 获取preNode的下一个兄弟节点,将其作为锚点
                  const anchor = preNode.el.nextSibling;
                  // 调用insert方法,将newNode插入到锚点元素前面
                  insert(newVNode.el, container, anchor);
              } else {
                // 否则,更新lastIndex
                lastIndex = j;
              }
              break;
            }
          }
        }
    }
  }

5.添加新元素

   对于需要新增的元素,我们同样也需要找到他在新元素列表中的位置。这里我们使用一个find变量,标记是否找到可复用元素,如果没有找到,说明该元素是需要新增的元素。

function patchChildren(n1, n2, container) {
    if (typeof n2.children === "string") {
      // ...
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children;
        const oldChildren = n1.children;
        const newChildren = n2.children;
		let lastIndex = 0; // 存储点前遍历的最大索引

        for (let i = 0; i < newChildren.length; i++) {
          const newVNode = newChildren[i];
          let find = false; // 标记是否找可复用元素
          for (let j = 0; j < oldChildren.length; j++) {
            const oldVNode = oldChildren[j];
            if (newVNode.key === oldVNode.key) {
              find = true; // 一旦找到立即标记
              patch(oldVNode, newVNode, container);
              if (j < lastIndex) {
                // 说明该节点需要移动
                const preNode = newChildren[i - 1];
                if (preNode) {
                  // 获取preNode的下一个兄弟节点,将其作为锚点
                  const anchor = preNode.el.nextSibling;
                  // 调用insert方法,将newNode插入到锚点元素前面
                  insert(newVNode.el, container, anchor);
                }
              } else {
                // 否则,更新lastIndex
                lastIndex = j;
              }
              break;
            }
          }
          // 如果没有找到可复用元素,说明当前需要挂载
          if (!find) {
            const preNode = newChildren[i - 1]; 
            let anchor = null; // 锚点
            if (preNode) {
              anchor = preNode.el.nextSibling;
            } else {
              // 如果是第一个元素,获取firstChild作为锚点
              anchor = preNode.el.firstChild;
            }
            patch(null, newVNode, container, anchor);
          }
        }
    }
  }

6.移除不存在的元素

   同样,对于新children中不存在的元素,我们同样需要进行移除。这里我们对oldChildren进行第二次遍历,查找newChildren是否存在oldChildren中元素的key,如果不存在,说明该元素需要被卸载。

function patchChildren(n1, n2, container) {
    if (typeof n2.children === "string") {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((child) => unmount(child));
      }
      setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
      if (Array.isArray(n1.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;
        let lastIndex = 0; // 存储点前遍历的最大索引

        for (let i = 0; i < newChildren.length; i++) {
          const newVNode = newChildren[i];
          let find = false; // 标记是否找可复用元素
          for (let j = 0; j < oldChildren.length; j++) {
            const oldVNode = oldChildren[j];
            if (newVNode.key === oldVNode.key) {
              find = true; // 一旦找到立即标记
              patch(oldVNode, newVNode, container);
              if (j < lastIndex) {
                // 说明该节点需要移动
                const preNode = newChildren[i - 1];
                if (preNode) {
                  // 获取preNode的下一个兄弟节点,将其作为锚点
                  const anchor = preNode.el.nextSibling;
                  // 调用insert方法,将newNode插入到锚点元素前面
                  insert(newVNode.el, container, anchor);
                }
              } else {
                // 否则,更新lastIndex
                lastIndex = j;
              }
              break;
            }
          }
          // 如果没有找到可复用元素,说明当前需要挂载
          if (!find) {
            const preNode = newChildren[i - 1];
            let anchor = null; // 锚点
            if (preNode) {
              anchor = preNode.el.nextSibling;
            } else {
              // 如果是第一个元素,获取firstChild作为锚点
              anchor = preNode.el.firstChild;
            }
            patch(null, newVNode, container, anchor);
          }
        }
        // 判断是否有需要移除的元素
        for (let i = 0; i < oldChildren.length; i++) {
          const oldVNode = oldChildren[i];
          const has = newChildren.find((vnode) => vnode.key === oldVNode.key);
          if (!has) {
            unmount(oldVNode);
          }
        }
      } else {
        setElementText(container, "");
        n2.children.forEach((child) => patch(null, child, container));
      }
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((child) => unmount(child));
      } else if (typeof n1.child === "string") {
        setElementText(container, "");
      }
    }
  }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

volit_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值