>웹 프론트엔드 >View.js >한 기사로 Vue Diff 알고리즘 이해하기

한 기사로 Vue Diff 알고리즘 이해하기

WBOY
WBOY앞으로
2022-10-07 08:00:291807검색

이 글에서는 diff 알고리즘과 관련된 문제를 주로 소개하는 vue에 대한 관련 지식을 제공합니다. diff 알고리즘의 기능은 두 vdom 노드 집합 간의 차이점을 찾아 가능한 한 많이 DOM 노드를 재사용하는 것입니다. 최소한의 성능 소모로 업데이트 작업을 완료할 수 있도록 하여 모두에게 도움이 되기를 바랍니다.

한 기사로 Vue Diff 알고리즘 이해하기

【관련 권장 사항: javascript 비디오 튜토리얼, vue.js 튜토리얼

왜 diff 알고리즘이 필요한가요?

일반적으로 사용되는 #app과 같은 컨테이너의 콘텐츠에는 일반적으로 세 가지 상황이 있습니다.

  • 문자열 유형, 즉 텍스트입니다.

  • 하위 노드 배열, 즉 하나 이상의 하위 노드를 포함합니다

  • null, 즉 하위 노드가 없습니다

vue에서 dom 요소는 vdom, HTML로 처리됩니다. 속성, 이벤트 바인딩 확실히 vdom에서 작동되고 최종적으로 실제 dom으로 렌더링됩니다.

Virtual Dom: 실제 DOM 노드를 설명하는 데 사용되는 JavaScript 개체입니다.

vdom을 사용하는 이유는 모든 작업을 실제 DOM에서 직접 수행하면 많은 오버헤드가 발생하기 때문입니다. vdom을 사용하면 실제 DOM 작업 수준에서 JavaScript 수준으로 성능 소비를 줄일 수 있어 상대적으로 더 좋습니다. 간단한 vdom은 다음과 같습니다.

const vdom = {
    type:"div",
    props:{
        class: "class",
        onClick: () => { console.log("click") }
    },
    children: [] // 简单理解这就是上述三种内容
}

vue 노드 업데이트의 경우 vdom을 비교에 사용합니다.

diff 알고리즘은 컨테이너 콘텐츠에 사용되는 두 번째 경우입니다. 업데이트 전 컨테이너의 콘텐츠가 하위 노드 집합이고 업데이트 후 콘텐츠가 여전히 노드 집합인 경우. diff 알고리즘을 사용하지 않는 경우 가장 간단한 작업은 이전 DOM을 모두 제거한 다음 현재 새 노드를 모두 마운트하는 것입니다.

하지만 dom 객체를 직접 조작하는 것은 성능 집약적이므로 diff 알고리즘의 역할은 두 vdom 노드 세트 간의 차이점을 찾고 dom 노드를 최대한 재사용하여 업데이트 작업이 다음과 같이 완료될 수 있도록 하는 것입니다. 최소한의 성능 소비.

다음으로 간단한 것부터 복잡한 것까지 세 가지 diff 알고리즘에 대해 이야기해 보겠습니다.

간단한 Diff 알고리즘

키가 왜 필요한가요?

다음으로 키가 필요한 이유를 두 가지 상황을 통해 설명해 보겠습니다.

다음과 같이 두 세트의 이전 노드 배열과 새 노드 배열이 있는 경우:

const oldChildren = [
    {type: 'p'},
    {type: 'span'},
    {type: 'div'},
]
const newChildren = [
    {type: 'div'},
    {type: 'p'},
    {type: 'span'},
    {type: 'footer'},
]

일반적인 비교를 수행하는 경우 단계는 다음과 같아야 합니다.

순환 비교를 위해 비교적 짧은 그룹을 찾습니다

  • 첫 번째 p 태그 div 태그와 일치하지 않습니다. 먼저 p 태그를 제거한 다음 div 태그를 마운트해야 합니다.

  • 첫 번째 스팸 태그가 p 태그와 일치하지 않습니다. 먼저 스팬 태그를 제거한 다음 p 태그를 마운트해야 합니다.

  • 첫 번째 div 태그가 스팬 태그와 일치하지 않습니다. 먼저 div 태그를 제거한 다음 스팬 태그를 마운트해야 합니다.

  • 마지막 추가 레이블 바닥글이 새 노드 배열에 존재하므로 마운트하기만 하면 됩니다.

그런 다음 7개의 DOM 작업이 수행되었지만 처음 세 개의 이름은 재사용할 수 있지만 위치가 변경되었음을 발견했습니다. 노드를 재사용하려면 두 노드가 동일한지 확인해야 하는데 현재의 기존 조건을 충족할 수 없습니다.

따라서 가상 노드의 ID 번호에 해당하는 키를 도입해야 합니다. 두 가상 노드의 유형과 키가 동일하다면 둘을 동일하다고 간주하여 DOM을 재사용할 수 있습니다.

이때 돔을 이동하기 위해 재사용된 요소를 찾을 수 있는데, 이는 지속적으로 노드를 마운트하고 언로드하는 것보다 상대적으로 더 좋습니다.

그러나 dom을 재사용한다고 해서 업데이트할 필요가 없다는 의미는 아닙니다.

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

위 노드는 동일한 유형과 키를 가지므로 재사용할 수 있으며 이때 하위 노드만 업데이트하면 됩니다.

간단한 diff 알고리즘 단계

먼저 예를 사용하여 전체 프로세스를 설명한 다음 방법을 설명합니다

const oldChildren = [
    {type: 'p', children: 'p', key: 1},
    {type: 'span', children: 'span', key: 2},
    {type: 'div', children: 'div', key: 3},
    {type: 'section', children: 'section', key : 4},
]
const newChildren = [
    {type: 'div', children: 'new div', key: 3},
    {type: 'p', children: 'p', key: 1},
    {type: 'span', children: 'span', key: 2},
    {type: 'footer', children: 'footer', key: 5},
]

설명의 단순화를 위해 여기에는 다른 레이블이 사용됩니다. 전체 과정은 다음과 같습니다:

한 기사로 Vue Diff 알고리즘 이해하기

  • 새 노드 배열에서 탐색을 시작합니다

  • 첫 번째는 div 태그이고 현재 첨자는 0이고 이전 첨자는 2입니다. 상대 위치는 변경되지 않았으며 이동이 필요하지 않으며 노드 내용만 업데이트하면 됩니다.

  • 두 번째는 p 태그이고, 현재 첨자는 1이고, 이전 첨자는 0입니다. 상대 위치 측면에서 p는 div 태그를 기준으로 변경되었으므로 이동해야 합니다. 이동된 위치는 div 태그 뒤입니다.

  • 第三个是span标签,当前的下标是2,之前的下标是1。就相对位置而言,p相对于div标签有变化,需要进行移动。移动的位置就是在p标签之后。

  • 第四个标签是footer,遍历旧节点数组发现并无匹配的元素。代表当前的元素是新节点,将其插入,插入的位置是span标签之后。

  • 最后一步,遍历旧节点数组,并去新节点数组中查找是否有对应的节点,没有则卸载当前的元素。

如何找到需要移动的元素?

上述声明了一个lastIdx变量,其初始值为0。作用是保存在新节点数组中,对于已经遍历了的新节点在旧节点数组的最大的下标。那么对于后续的新节点来说,只要它在旧节点数组中的下标的值小于当前的lastIdx,代表当前的节点相对位置发生了改变,则需要移动,

举个例子:div在旧节点数组中的位置为2,大于当前的lastIdx,更新其值为2。对于span标签,它的旧节点数组位置为1,其值更小。又因为当前在新节点数组中处于div标签之后,就是相对位置发生了变化,便需要移动。

当然,lastIdx需要动态维护。

总结

简单diff算法便是拿新节点数组中的节点去旧节点数组中查找,通过key来判断是否可以复用。并记录当前的lastIdx,以此来判断节点间的相对位置是否发生变化,如果变化,需要进行移动。

双端diff算法

简单diff算法并不是最优秀的,它是通过双重循环来遍历找到相同key的节点。举个例子:

const oldChildren = [
    {type: 'p', children: 'p', key: 1},
    {type: 'span', children: 'span', key: 2},
    {type: 'div', children: 'div', key: 3},
]
const newChildren = [
    {type: 'div', children: 'new div', key: 3},
    {type: 'p', children: 'p', key: 1},
    {type: 'span', children: 'span', key: 2},
]

其实不难发现,我们只需要将div标签节点移动即可,即进行一次移动。不需要重复移动前两个标签也就是p、span标签。而简单diff算法的比较策略即是从头至尾的循环比较策略,具有一定的缺陷。

顾名思义,双端diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法

한 기사로 Vue Diff 알고리즘 이해하기

那么双端diff算法开始的步骤如下:

  • 比较 oldStartIdx节点 与 newStartIdx 节点,相同则复用并更新,否则

  • 比较 oldEndIdx节点 与 newEndIdx 节点,相同则复用并更新,否则

  • 比较 oldStartIdx节点 与 newEndIdx 节点,相同则复用并更新,否则

  • 比较 oldEndIdx节点 与 newStartIdx 节点,相同则复用并更新,否则

简单概括:

  • 旧头 === 新头?复用,不需移动

  • 旧尾 === 新尾?复用,不需移动

  • 旧头 === 新尾?复用,需要移动

  • 旧尾 === 新头?复用,需要移动

对于上述例子而言,比较步骤如下:

한 기사로 Vue Diff 알고리즘 이해하기

上述的情况是一种非常理想的情况,我们可以根据现有的diff算法完全的处理两组节点,因为每一轮的双端比较都会命中其中一种情况使得其可以完成处理。

但往往会有其他的情况,比如下面这个例子:

const oldChildren = [
    {type: 'p', children: 'p', key: 1},
    {type: 'span', children: 'span', key: 2},
    {type: 'div', children: 'div', key: 3},
    {type: 'ul', children: 'ul', key: 4},
]
const newChildren = [
    {type: 'div', children: 'new div', key: 3},
    {type: 'p', children: 'p', key: 1},
    {type: 'ul', children: 'ul', key: 4},
    {type: 'span', children: 'span', key: 2},
]

此时我们会发现,上述的四个步骤都会无法命中任意一步。所以需要额外的步骤进行处理。即是:在四步比较失败后,找到新头节点在旧节点中的位置,并进行移动即可。动图示意如下:

한 기사로 Vue Diff 알고리즘 이해하기

当然还有删除、增加等均不满足上述例子的操作,但操作核心一致,这里便不再赘述。

总结

双端diff算法的优势在于对于一些比较特殊的情况能更快的对节点进行处理,也更贴合实际开发。而双端的含义便在于通过两组子节点的头尾分别进行比较并更新。

快速diff算法

首先,快速diff算法包含了预处理步骤。它借鉴了纯文本diff的思路,这时它为何快的原因之一。

比如:

const text1 = '我是快速diff算法'
const text2 =  '我是双端diff算法'

那么就会先从头比较并去除可用元素,其次会重后比较相同元素并复用,那么结果就会如下:

const text1 = '快速'
const text2 = '双端'

此时再进行一些其他的比较和处理便会简单很多。

其次,快速diff算法还使用了一种算法来尽可能的复用dom节点,这个便是最长递增子序列算法。为什么要用呢?先举个例子:

// oldVNodes
const vnodes1 = [
    {type:'p', children: 'p1', key: 1},
    {type:'div', children: 'div', key: 2},
    {type:'span', children: 'span', key: 3},
    {type:'input', children: 'input', key: 4},
    {type:'a', children: 'a', key: 6}
    {type:'p', children: 'p2', key: 5},
]
// newVNodes 
const vnodes2 = [
    {type:'p', children: 'p1', key: 1},
    {type:'span', children: 'span', key: 3},
    {type:'div', children: 'div', key: 2},
    {type:'input', children: 'input', key: 4},
    {type:'p', children: 'p2', key: 5},
]

经过预处理步骤之后得到的节点如下:

// oldVNodes
const vnodes1 = [
    {type:'div', children: 'div', key: 2},
    {type:'span', children: 'span', key: 3},
    {type:'input', children: 'input', key: 4},
    {type:'a', children: 'a', key: 6},
]
// newVNodes 
const vnodes2 = [
    {type:'span', children: 'span', key: 3},
    {type:'div', children: 'div', key: 2},
    {type:'input', children: 'input', key: 4},
]

此时我们需要获得newVNodes节点相对应oldVNodes节点中的下标位置,我们可以采用一个source数组,先循环遍历一次newVNodes,得到他们的key,再循环遍历一次oldVNodes,获取对应的下标关系,如下:

const source = new Array(restArr.length).fill(-1)
// 处理后
source = [1, 2, 0, -1]

注意!这里的下标并不是完全正确!因为这是预处理后的下标,并不是刚开始的对应的下标值。此处仅是方便讲解。 其次,source数组的长度是剩余的newVNodes的长度,若在处理完之后它的值仍然是-1则说明当前的key对应的节点在旧节点数组中没有,即是新增的节点。

此时我们便可以通过source求得最长的递增子序列的值为 [1, 2] 。对于index为1,2的两个节点来说,他们的相对位置在原oldVNodes中是没有变化的,那么便不需要移动他们,只需要移动其余的元素。这样便能达到最大复用dom的效果。

步骤

以上述例子来说:

  • 首先进行预处理

한 기사로 Vue Diff 알고리즘 이해하기

注意!预处理过的节点虽然复用,但仍然需要进行更新。

  • 进行source填充

한 기사로 Vue Diff 알고리즘 이해하기

当然这里递增子序列 [1, 2] 和 [0, 1]都是可以的。

  • 进行节点移动

用索引i指向新节点数组中的最后一个元素

用索引s指向最长递增子序列中的最后一个元素

然后循环进行以下步骤比较:

source[i] === -1?等于代表新节点,挂载即可。随后移动i

i === 递增数组[s]? 等于代表当前的节点存在在递增子序列中,是复用的节点,当前的节点无需移动。

上述均不成立代表需要移动节点。

한 기사로 Vue Diff 알고리즘 이해하기

节点更新,结束。

总结

核心思路便是进行了类似文本预处理的步骤去除头尾重复的节点。其次便是采用了最长递增子序列来复用相对位置没有发生变化的节点,这些节点是不需要移动的,便能最快的复用和更新。

最后

其实不论是哪一种diff算法,它都需要遵循同样的处理规则:

判断哪些节点需要移动,以及如何移动。

找出那些需要被添加的或者被移除的节点。

【相关推荐:javascript视频教程vue.js教程

위 내용은 한 기사로 Vue Diff 알고리즘 이해하기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제