《Vuejs设计与实现》第 9 章(简单 diff 算法)

目录

9.1 减少 DOM 操作的性能开销

9.2 DOM 复用与 key 的作用

9.3 找到需要移动的元素

9.4 如何移动元素

9.5 添加新元素

9.6 移除不存在的元素

9.7 总结


当新旧 vnode 的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点,用于比较的算法就叫作 Diff 算法。

9.1 减少 DOM 操作的性能开销

之前我们在更新子节点时,简单地移除所有旧的子节点,然后添加所有新的子节点。
这种方式虽然简单直接,但会产生大量的性能开销,因为它没有复用任何 DOM 元素。
考虑下面的新旧虚拟节点示例:

// 旧 vnode
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1' },{ type: 'p', children: '2' },{ type: 'p', children: '3' }]
}// 新 vnode
const newVNode = {type: 'div',children: [{ type: 'p', children: '4' },{ type: 'p', children: '5' },{ type: 'p', children: '6' }]
}

上述代码,我们会执行六次操作,三次卸载旧节点,三次添加新节点,但实际上,旧新节点都是 'p' 标签,只是它们的文本内容变了。
理想情况下,我们只需要更新这些 'p' 标签的文本内容就可以了,这样只需要 3 次 DOM 操作,性能提升了一倍。
我们可以调整 patchChildren 函数,让它只更新变化的部分:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {// 重新实现两组子节点的更新方式// 新旧 childrenconst oldChildren = n1.childrenconst newChildren = n2.children// 遍历旧的 childrenfor (let i = 0; i < oldChildren.length; i++) {// 调用 patch 函数逐个更新子节点patch(oldChildren[i], newChildren[i])}} else {// 省略部分代码}
}

上述代码,patch 函数在执行更新时,发现新旧子节点只有文本内容不同,因此只会更新其文本节点的内容
 

image.png

但是这段代码假设新旧子节点的数量总是一样的,实际上新旧节点的数量可能发生变化,如果新节点较多,我们应该添加节点,反之则删除节点。
所以,我们应遍历长度较短的那组子节点,以便尽可能多地调用 patch 函数进行更新。然后,比较新旧子节点组的长度,如果新组长度更长,就挂载新子节点;反之,就卸载旧子节点

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst 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,说明有新子节点需要挂载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])}}} else {// 省略部分代码}
}

这样,无论新旧子节点组的数量如何,我们的渲染器都能正确地挂载或卸载它们。

9.2 DOM 复用与 key 的作用

上面我们通过减少操作次数提高了性能,但仍有优化空间。
以新旧两组子节点为例,它们的内容如下:

// oldChildren
[{ type: 'p' },{ type: 'div' },{ type: 'span' }
]// newChildren
[{ type: 'span' },{ type: 'p' },{ type: 'div' }
]

若使用之前的算法更新子节点,需要执行6次 DOM 操作。观察新旧子节点,发现它们只是顺序不同。
因此,最优处理方式是通过 DOM 移动来完成更新,而非频繁卸载和挂载。为实现这一目标,需确保新旧子节点中存在可复用节点。
为判断新子节点是否在旧子节点中出现,可以引入 key 属性作为虚拟节点的标识。只要两个虚拟节点的 type 和 key 属性相同,我们认为它们相同,可以复用DOM。例如:

image.png

// oldChildren
[{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 },{ type: 'p', children: '3', key: 3 }
]// newChildren
[{ type: 'p', children: '3', key: 3 },{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 }
]

 我们根据子节点的 key 属性,能够明确知道新子节点在旧子节点中的位置,这样就可以进行相应的 DOM 移动操作了。
注意 DOM 可复用并不意味着不需要更新,它可能内部子节点不一样:

const oldVNode = { type: 'p', key: 1, children: 'text 1' }
const newVNode = { type: 'p', key: 1, children: 'text 2' }

所以补丁操作是在移动 DOM 元素之前必须完成的步骤,如下面的 patchChildren 函数所示:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.children// 遍历新的 childrenfor (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]// 遍历旧的 childrenfor (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]// 如果找到了具有相同 key 值的两个节点,说明可以复用,但仍然需要调用 patch 函数更新if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container)break // 这里需要 break}}}} else {// 省略部分代码}
}

在这段代码中,我们更新了新旧两组子节点。通过两层 for 循环,外层遍历新的子节点,内层遍历旧的子节点,我们寻找并更新了所有可复用的节点。
例如,有如下的新旧两组子节点:

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 }]
}// 首次挂载
renderer.render(oldVNode, document.querySelector('#app'))
setTimeout(() => {// 1 秒钟后更新renderer.render(newVNode, document.querySelector('#app'))
}, 1000);

运行上述代码,1 秒钟后,key 为 3 的子节点对应的真实 DOM 的文本内容将从 'hello' 更新为 'world'。让我们仔细分析一下这段代码在执行更新操作时的过程:

  • 第一步,我们选取新的子节点组中的第一个子节点,即 key 为 3 的节点。然后在旧的子节点组中寻找具有相同 key 的节点。我们发现,旧子节点 oldVNode[2] 的 key 为 3,因此调用 patch 函数进行补丁操作。这个操作完成后,渲染器会将 key 为 3 的虚拟节点对应的真实 DOM 的文本内容从 'hello' 更新为 'world'。
  • 第二步,我们取新的子节点组中的第二个子节点,即 key 为 1 的节点,并在旧的子节点组中寻找具有相同 key 的节点。我们发现,旧的子节点 oldVNode[0] 的 key 为 1,于是再次调用 patch 函数进行补丁操作。由于 key 为 1 的新旧子节点没有任何差异,所以这里并未进行任何操作。
  • 第三步,最后,我们取新的子节点组中的最后一个子节点,即 key 为 2 的节点,这一步的结果与第二步相同。

经过以上更新操作后,所有节点对应的真实 DOM 元素都已更新。
但真实 DOM 仍保持旧的子节点顺序,即 key 为 3 的节点对应的真实 DOM 仍然是最后一个子节点。
然而在新的子节点组中,key 为 3 的节点已经变为第一个子节点,因此我们还需要通过移动节点来完成真实 DOM 顺序的更新。

9.3 找到需要移动的元素

现在,我们已经能够通过 key 值找到可复用的节点了。
下一步,我们确定哪些节点需要移动以及如何移动。我们逆向思考下,什么条件下节点无需移动。答案直观:新旧子节点顺序未变时,无需额外操作:

image.png


新旧子节点顺序未变,举例说明旧子节点索引:

  • key 为 1 的节点在旧 children 数组中的索引为 0;
  • key 为 2 的节点在旧 children 数组中的索引为 1;
  • key 为 3 的节点在旧 children 数组中的索引为 2。

应用我们上节的更新算法:

  • 第一步:取新的一组子节点中的第一个节点 p-1,它的 key 为 1。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 0。
  • 第二步:取新的一组子节点中的第二个节点 p-2,它的 key 为 2。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 1。
  • 第三步:取新的一组子节点中的第三个节点 p-3,它的 key 为 3。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 2。

如果每次找到可复用节点,记录他们原先在旧子节点的位置索引,把这些位置索引值按照先后顺序排列,则可以得到一个序列:0、1、2。这是一个递增的序列,在这种情况下不需要移动任何节点。
我们再来看看另外一个例子:

image.png

  • 第一步:取新的一组子节点中的第一个节点 p-3,它的 key 为 3。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 2。
  • 第二步:取新的一组子节点中的第二个节点 p-1,它的 key 为 1。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 0。
    • 到了这一步我们发现,索引值递增的顺序被打破了。节点 p-1 在旧 children 中的索引是 0,它小于节点 p-3 在旧 children 中的索引 2。这说明节点 p-1 在旧 children 中排在节点 p-3 前面,但在新的 children 中,它排在节点 p-3 后面。因此,我们能够得出一个结论:节点 p-1 对应的真实 DOM 需要移动。
  • 第三步:取新的一组子节点中的第三个节点 p-2,它的 key 为 2。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 1。
    • 到了这一步我们发现,节点 p-2 在旧 children 中的索引 1 要小于节点 p-3 在旧 children 中的索引 2。这说明,节点 p-2 在旧 children 中排在节点 p-3 前面,但在新的 children 中,它排在节点 p-3 后面。因此,节点 p-2 对应的真实 DOM 也需要移动。

在上面的例子中,我们得出了节点 p-1 和节点 p-2 需要移动的结论。这是因为它们在旧 children 中的索引要小于节点 p-3 在旧 children 中的索引。如果我们按照先后顺序记录在寻找节点过程中所遇到的位置索引,将会得到序列:2、0、1。可以发现,这个序列不具有递增的趋势。
我们可以用 lastIndex 变量存储整个寻找过程中遇到的最大索引值,如下面的代码所示:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.children// 用来存储寻找过程中遇到的最大索引值let lastIndex = 0for (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 // 这里需要 break}}}} else {// 省略部分代码}
}

上述代码,如果新旧节点的 key 值相同,我们就找到了可以复用的节点。我们比较这个节点在旧子节点数组中的索引 j 与 lastIndex。
如果 j 小于 lastIndex,说明当前 oldVNode 对应的真实 DOM 需要移动。
否则,不需要移动。并将变量 j 的值赋给 lastIndex,以确保寻找节点过程中,变量 lastIndex 始终存储着当前遇到的最大索引值。

9.4 如何移动元素

移动节点指的是,移动一个虚拟节点所对应的真实 DOM 节点,并不是移动虚拟节点本身。
既然移动的是真实 DOM 节点,那么就需要取得对它的引用才行。当一个虚拟节点被挂载后,其对应的真实 DOM 节点会存储在它的 vnode.el 属性中:

image.png


因此,我们可以通过 vnode.el 属性取得它对应的真实 DOM 节点。

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

function patchElement(n1, n2) {// 新的 vnode 也引用了真实 DOM 元素const el = n2.el = n1.el// 省略部分代码
}

上述代码 patchElement 函数首先将旧节点的 n1.el 属性赋值给新节点的 n2.el 属性,这个赋值的意义其实就是 DOM 元素的复用。
在复用了 DOM 元素之后,新节点也将持有对真实 DOM 的引用:

image.png


此时无论是新旧节点,都引用着真实 DOM,在此基础上,我们就可以进行 DOM 移动了。

为了阐述如何移动 DOM,,我们仍然引用上一节的更新案例:

image.png


它的更新步骤如下。

  • 第一步:在新的子节点集合中选取第一个节点 p-3(key 为 3),并在旧的子节点集合中寻找具有相同 key 的可复用节点。找到了这样的节点,其在旧集合中的索引为 2。由于当前 lastIndex 为 0,且 2 大于 0,因此,p-3 的实际 DOM 无需移动,但需要将 lastIndex 更新为 2。
  • 第二步:选取新集合中的第二个节点 p-1(key 为 1),并尝试在旧集合中找到相同 key 的可复用节点。找到了这样的节点,其在旧集合中的索引为 0。此时,由于 lastIndex 为 2,且 0 小于 2,所以,p-1 的实际 DOM 需要移动。此时我们知道,**新 children 的顺序即为更新后实际 DOM 应有的顺序。**因此,p-1 在新 children 中的位置决定了其在更新后实际 DOM 中的位置。由于 p-1 在新 children 中排在 p-3 后面,因此我们需要将 p-1 的实际 DOM 移动到 p-3 的实际 DOM 后面。移动后的实际 DOM 顺序为 p-2、p-3、p-1:
  • image.png

    把节点 p-1 对应的真实 DOM 移动到节点 p-3 对应的真实 DOM 后面
  • 第三步:选取新集合中的第三个节点 p-2(key 为 2),并尝试在旧集合中找到相同 key 的可复用节点。找到了这样的节点,其在旧集合中的索引为1。此时,由于 lastIndex 为 2,且 1 小于 2,所以,p-2 的实际 DOM 需要移动。此步骤与步骤二类似,我们需要将 p-2 的实际 DOM 移动到 p-1 的实际 DOM 后面。经过移动后,实际 DOM 的顺序与新的子节点集合的顺序相同,即为:p-3、p-1、p-2。至此,更新操作完成:
  • image.png

    把节点 p-2 对应的真实 DOM 移动到节点 p-1 对应的真实 DOM 后面

接下来,我们来看一下如何实现这个过程。具体的代码如下:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]let j = 0for (j; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container)if (j < lastIndex) {// 代码运行到这里,说明 newVNode 对应的真实 DOM 需要移动// 先获取 newVNode 的前一个 vnode,即 prevVNodeconst prevVNode = newChildren[i - 1]// 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,它不需要移动if (prevVNode) {// 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点const anchor = prevVNode.el.nextSibling// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,// 也就是 prevVNode 对应真实 DOM 的后面insert(newVNode.el, container, anchor)}} else {lastIndex = j}break}}}} else {// 省略部分代码}
}

上述代码中,如果 j < lastIndex 成立,则说明当前 newVNode 对应的真实 DOM 需要移动。
根据之前的分析可知,我们需要获取当前 newVNode 节点的前一个虚拟节点 newChildren[i - 1],然后使用 insert 函数完成节点的移动,其中 insert 函数依赖浏览器原生的 insertBefore 函数。如下所示:

const renderer = createRenderer({// 省略部分代码insert(el, parent, anchor = null) {// insertBefore 需要锚点元素 anchorparent.insertBefore(el, anchor)}// 省略部分代码
})

9.5 添加新元素

image.png


上图,我们有一个新的节点p-4, 它的 key 值为 4,这个节点在旧的节点集中不存在。该新增节点我们应该挂载:

  1. 找到新节点
  2. 将新节点挂载到正确位置

image.png


根据上图,我们开始模拟执行简单 Diff 算法的更新逻辑:

  • 第一步:我们首先检查新的节点集中的第一个节点 p-3。这个节点在旧的节点集中存在,因此我们不需要移动对应的 DOM 元素,但是我们需要将变量lastIndex的值更新为 2。
  • 第二步:取新的一组子节点中第二个节点 p-1,它的 key 值为 1,尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 0。此时变量 lastIndex 的值为 2,索引值 0 小于 lastIndex 的值 2,所以节点 p-1 对应的真实 DOM 需要移动,并且应该移动到节点 p-3 对应的真实DOM 后面。移动后,DOM 的顺序将变为 p-2、p-3、p-1:
  • image.png

  • 第三步:我们现在查看新的节点集中的第三个节点 p-4。在旧的节点集中,我们找不到这个节点,我们需要观察节点 p-4 在新的一组子节点中的位置。由于节点 p-4 出现在节点 p-1 后面,所以我们应该把节点 p-4 挂载到节点 p-1 所对应的真实 DOM 后面。DOM 元素后面。挂载后,DOM的顺序将变为 p-2、p-3、p-1、p-4:
  • image.png

  • 第四步:最后,我们查看新的节点集中的第四个节点 p-2。在旧的节点集中,这个节点的索引值为 1,这个值小于 lastIndex 的值 2,因此我们需要移动 p-2 对应的 DOM 元素。应该移动到节点 p-4 对应的真实DOM 后面。
  • image.png

在此,我们看到真实 DOM 的顺序为:p-3、p-1、p-4、p-2。这表明真实 DOM 的顺序已经与新子节点的顺序一致,更新已经完成。
接下来,让我们通过 patchChildren 函数的代码实现来详细讲解:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]let j = 0// 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,// 初始值为 false,代表没找到let find = falsefor (j; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]if (newVNode.key === oldVNode.key) {// 一旦找到可复用的节点,则将变量 find 的值设为 truefind = truepatch(oldVNode, newVNode, container)if (j < lastIndex) {const prevVNode = newChildren[i - 1]if (prevVNode) {const anchor = prevVNode.el.nextSiblinginsert(newVNode.el, container, anchor)}} else {lastIndex = j}break}}// 如果代码运行到这里,find 仍然为 false,// 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点// 也就是说,当前 newVNode 是新增节点,需要挂载if (!find) {// 为了将节点挂载到正确位置,我们需要先获取锚点元素// 首先获取当前 newVNode 的前一个 vnode 节点const prevVNode = newChildren[i - 1]let anchor = nullif (prevVNode) {// 如果有前一个 vnode 节点,则使用它的下一个兄弟节点作为锚点元素anchor = prevVNode.el.nextSibling} else {// 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节点// 这时我们使用容器元素的 firstChild 作为锚点anchor = container.firstChild}// 挂载 newVNodepatch(null, newVNode, container, anchor)}}} else {// 省略部分代码}
}

上述代码,我们通过外层循环中定义的变量 find,查找是否存在可复用的节点。
如果内层循环结束后,find 的值仍为 false,说明当前 newVNode 是全新的节点,需要进行挂载。
挂载的位置由 anchor 确定,这个 anchor 可以是 newVNode 的前一个虚拟节点的下一个兄弟节点,或者容器元素的第一个子节点。
现在,我们需要调整 patch 函数以支持接收第四个参数 anchor,如下所示:

// patch 函数需要接收第四个参数,即锚点元素
function patch(n1, n2, container, anchor) {// 省略部分代码if (typeof type === 'string') {if (!n1) {// 挂载时将锚点元素作为第三个参数传递给 mountElement 函数mountElement(n2, container, anchor)} else {patchElement(n1, n2)}} else if (type === Text) {// 省略部分代码} else if (type === Fragment) {// 省略部分代码}
}// mountElement 函数需要增加第三个参数,即锚点元素
function mountElement(vnode, container, anchor) {// 省略部分代码// 在插入节点时,将锚点元素透传给 insert 函数insert(el, container, anchor)
}

9.6 移除不存在的元素

在更新子节点时,不仅会遇到新增元素,还会出现元素被删除的情况:
 

image.png


假设在新的子节点组中,节点 p-2 已经不存在,这说明该节点被删除了。

我们像上面一样模拟执行更新逻辑,这之前我们先看看新旧两组子节点以及真实 DOM 节点的当前状态:

image.png

  • 第一步:取新的子节点组中的第一个节点 p-3,它的 key 值为 3。在旧的子节点组中寻找可复用的节点,发现索引为 2 的节点可复用,此时变量 lastIndex 的值为 0,索引 2 不小于 lastIndex 的值 0,所以 节点 p-3 对应的真实 DOM 不需要移动,但需要更新变量 lastIndex 的值为 2。
  • 第二步,取新的子节点组中的第二个节点 p-1,它的 key 值为 1。在旧的子节点组中发现索引为 0 的节点可复用。 并且该节点在旧的一组子节点中的索引值为 0。此时变量 lastIndex 的值为 2,索引 0 小于 lastIndex 的值 2,所以节 点 p-1 对应的真实 DOM 需要移动,并且应该移动到节点 p-3 对 应的真实 DOM 后面:
  • image.png

  • 最后,我们发现节点 p-2 对应的真实 DOM 仍然存在,所以需要增加逻辑来删除遗留节点。

我们可以在基本更新结束后,遍历旧的子节点组,然后去新的子节点组中寻找具有相同 key 值的节点。如果找不到,说明应删除该节点,如下面 patchChildren 函数的代码所示:

function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代码} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {// 省略部分代码}// 上一步的更新操作完成后// 遍历旧的一组子节点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) {// 如果没有找到具有相同 key 值的节点,则说明需要删除该节点// 调用 unmount 函数将其卸载unmount(oldVNode)}}} else {// 省略部分代码}
}

上述代码,在上一步的更新操作完成之后,我们还需要遍历旧的一组子节点,目的是检查旧子节点在新的一组子节点中是否仍然存在,如果已经不存在了,则调用 unmount 函数将其卸载。

9.7 总结

本章我们讨论 Diff 算法的作用,Diff 是用来计算两组子节点的差异,并最大程度复用 DOM 元素。
最开始我们采用了一种简单的方式来更新子节点,即卸载所有旧子节点,再挂载所有新子节点。然而这种操作无疑是非常消耗性能的。
于是我们改进为:遍历新旧两组子节点中数量较少的那一组,并逐个调用 patch 函数进行打补丁,然后比较新旧两组子节点的数量,如果新的一组子节点数量更多,说明有新子节点需要挂载;否则说明在旧的一组子节点中,有节点需要卸载。
然后我们讨论了 key 值作用,,它就像虚拟节点 的“身份证号”。渲染器通过 key 找到可复用元素,避免对 DOM 元素过多的销毁重建。
接着我们讨论了简单 Diff 逻辑:在新的一组节点中去寻找旧节点可复用的元素。如果找到了,则记录该节点的位置索引。我们把这个位置索引称为最大索引。在整个更新过程中,如果一个节点的索引值小于最大索引,则说明该节点对应的真实 DOM 元素需要移动。
最后,我们讲解了渲染器是如何移动、添加、删除 虚拟节点所对应的 DOM 元素的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/84323.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/84323.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

队列,环形缓冲区实现与应用:适用于GD32串口编程或嵌入式底层驱动开发

环形缓冲区实现与应用&#xff1a;从基础到实践 在嵌入式系统和实时数据处理场景中&#xff0c;环形缓冲区&#xff08;Circular Buffer&#xff09;是一种非常常用的的数据结构&#xff0c;它能有效地管理数据的读写操作&#xff0c;尤其适用于数据流的临时存储与转发。 今天…

WHAT - Expo Go 和 development build

文章目录 1. 什么是 Expo Go?简介作用限制2. 什么是 Development Build(开发构建)?简介功能创建方式3. 它们有什么区别?总结建议怎么从 Expo Go 迁移到开发构建一、什么是“迁移”?二、迁移步骤总览三、详细操作步骤1. 安装 expo-dev-client2. 配置 eas.json(Expo 应用服…

Keepalived 配置 VIP 的核心步骤

Keepalived 配置 VIP 的核心步骤主要涉及安装软件、主备节点配置及服务管理。以下是具体操作指南: 一、安装 Keepalived ‌Ubuntu/Debian 系统‌ sudo apt update sudo apt install keepalived ‌CentOS/RHEL 系统‌ sudo yum install keepalived 注:需确保已配置 EPE…

HarmonyOS 5折叠屏自适应广告位布局方案详解

以下是HarmonyOS 5折叠屏广告位自适应布局的完整技术方案&#xff0c;综合响应式设计、动态交互与元服务融合策略&#xff1a; 一、核心布局技术‌ ‌断点响应式设计‌ 基于屏幕宽度动态调整布局结构&#xff0c;避免简单拉伸&#xff1a; // 定义断点阈值&#xff08;单位&am…

【数据分析十:Classification prediction】分类预测

一、分类的定义 已知&#xff1a;一组数据&#xff08;训练集&#xff09; (X, Y) 例如&#xff1a; x&#xff1a;数据特征/属性&#xff08;如收入&#xff09; y&#xff1a;类别标记&#xff08;是否有借款&#xff09; 任务: 学习一个模型&#xff0c;利用每一条记录…

设计模式-接口隔离原则(Interface Segregation Principle, ISP)

接口隔离原则&#xff08;Interface Segregation Principle, ISP&#xff09; 核心思想&#xff1a;客户端不应被迫依赖它们不使用的接口方法。 目标&#xff1a;通过拆分臃肿的接口为更小、更具体的接口&#xff0c;减少不必要的依赖&#xff0c;提高系统的灵活性和可维护性。…

超融合:系统工程还是软件工程? 从H3C UIS9.0看超融合的技术本质

在数字化转型的浪潮中&#xff0c;超融合基础架构&#xff08;Hyper-Converged Infrastructure, HCI&#xff09;凭借其简化部署、弹性扩展和高效运维的优势&#xff0c;成为企业IT基础设施升级的重要选择。 然而&#xff0c;关于超融合究竟属于系统工程还是软件工程的讨论一直…

青少年编程与数学 01-012 通用应用软件简介 01 Microsoft Office办公软件

青少年编程与数学 01-012 通用应用软件简介 01 Microsoft Office办公软件 **一、Microsoft Office办公软件概述****二、发展过程**&#xff08;一&#xff09;早期起源&#xff08;二&#xff09;技术演进 **三、主要用途或功能**&#xff08;一&#xff09;文字处理&#xff0…

vivado IP综合选项

在 Vivado 中&#xff0c;生成 IP 文件时的 Synthesis Options 提供了两种主要的综合模式&#xff1a;Global 和 Out of Context per IP。这两种模式的主要区别如下&#xff1a; 1. Global Synthesis&#xff08;全局综合&#xff09; 定义&#xff1a;在这种模式下&#xff…

零信任一招解决智慧校园的远程访问、数据防泄露、安全运维难题

随着数字化转型持续深入&#xff0c;“智慧校园”已成为高校发展的必经之路。从统一门户、一卡通到教务系统、选课系统&#xff0c;各类应用极大地便利了师生的工作与学习。 然而&#xff0c;便捷的背后也隐藏着一系列安全挑战。为了满足师生校外访问的需求&#xff0c;许多应…

web布局08

flex-basis 是 Flexbox 布局模块中 flex 属性的另一个子属性&#xff0c;在前面的课程中我们深度剖析了浏览器是如何计算 Flex 项目尺寸的&#xff0c;或者说 Flexbox 是如何工作的。对于众多 Web 开发者而言&#xff0c;在 CSS 中都习惯于使用像 width 、height 、min-* 和 ma…

在 Docker 27.3.1 中安装 PostgreSQL 16 的实践

前言&#xff1a;为什么在 Docker 中部署 PostgreSQL&#xff1f; 在云原生时代&#xff0c;容器化部署已成为生产环境的首选方案。通过 Docker 部署 PostgreSQL 具有以下显著优势&#xff1a; 环境一致性&#xff1a;消除“在我机器上能运行”的问题快速部署&#xff1a;秒级…

日志混乱与数据不一致问题实战排查:工具协同调试记录(含克魔使用点)

日志调试、状态验证和数据一致性排查&#xff0c;是iOS开发中最费时间、最易出错的工作之一。尤其是在模块之间异步通信频繁、本地缓存与远程状态需保持同步时&#xff0c;如果缺乏一套合适的流程与工具&#xff0c;开发人员极容易陷入“盲查状态”。 在一次跨部门联合开发的A…

Redis底层数据结构与内部实现

目录 一、RedisDB结构 1、RedisDB在Redis实例中的位置 2、RedisDB结构与核心组件 二、RedisObject结构 1、核心数据结构 1.1 简单动态字符串 (Simple Dynamic String - SDS) 1.2 字典 (Dict / Hash Table) 1.3 双端链表 (Linked List) 1.4 跳跃表 (Skip List) 1.5 压…

【项目实训】【项目博客#07】HarmonySmartCodingSystem系统前端开发技术详解(5.12-6.15)

【项目实训】【项目博客#07】HarmonySmartCodingSystem系统前端开发技术详解&#xff08;5.12-6.15&#xff09; 一、项目概述与目标 HarmonySmartCodingSystem是一个面向HarmonyOS开发者的智能编码辅助平台&#xff0c;旨在通过自然语言交互简化开发流程&#xff0c;提供智能…

系统性能优化-2 CPU

系统性能优化-2 CPU 其实除了 CPU 的频率&#xff0c;多核架构以及多 CPU 架构对系统运行的性能也是很大影响的&#xff0c;那么该如何充分利用 CPU 呢&#xff1f; CPU 架构 首先介绍一下当前主流的 CPU 架构&#xff0c;现在的系统基本都是多 CPU&#xff0c;一个 CPU 处理…

Docker Pull 相关配置指南

在Docker环境中&#xff0c;docker pull命令用于从Docker镜像仓库拉取镜像。为了确保Docker镜像能够快速、稳定地拉取&#xff0c;配置 docker pull相关的设置是非常重要的。本文将详细介绍如何配置Docker以优化 docker pull操作&#xff0c;涵盖镜像源配置、登录私有仓库、网络…

Python的Matplotlib库:从入门到精通的数据可视化实战指南

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 持续学习&#xff0c;不断…

CentOS查日志

在 CentOS 系统中&#xff0c;查看日志是系统维护和故障排查的重要技能。以下是常用的日志查看方法和工具&#xff1a; 1. 基本日志位置 CentOS 使用systemd管理服务&#xff0c;主要日志存储在&#xff1a; /var/log/messages&#xff1a;系统主日志/var/log/secure&#x…

Linux运维新人自用笔记(用虚拟机Ubuntu部署lamp环境,搭建WordPress博客)

内容全为个人理解和自查资料梳理&#xff0c;欢迎各位大神指点&#xff01; 每天学习较为零散。 day20 一、./configure 脚本命令 ./configure 是 Unix/Linux 系统中用于配置软件源代码的脚本命令&#xff0c;通常用于为后续的 make 和 make install 准备编译环境。 选项作…