Maison > Article > interface Web > Comprendre rapidement l'algorithme de comparaison Vue2 (explication graphique détaillée)
L'algorithme diff est un algorithme efficace qui compare les nœuds d'arbre au même niveau, évitant ainsi d'avoir à rechercher et parcourir l'arbre couche par couche. Alors, que savez-vous de l’algorithme de comparaison ? L'article suivant vous donnera une analyse approfondie de l'algorithme diff de vue2. J'espère qu'il vous sera utile !
Je regarde le code source de Vue 2 depuis longtemps De l'utilisation de Flow à l'utilisation actuelle de TypeScript, j'ouvrirai son code source à chaque fois pour y jeter un œil, mais à chaque fois je ne vois que le . l'initialisation des données aussi. C'est l'étape de beforeMount
. Je n'ai jamais étudié attentivement comment générer un VNode (Visual Dom Node, qui peut aussi être directement appelé vdom) et comment comparer un VNode (diff). ) lors de la mise à jour des composants. Je sais seulement que beforeMount
的阶段,对于如何生成 VNode(Visual Dom Node, 也可以直接称为 vdom) 以及组件更新时如何比较 VNode(diff)始终没有仔细研究,只知道采用了 双端 diff 算法,至于这个双端是怎么开始怎么结束的也一直没有去看过,所以这次趁写文章的机会仔细研究一下。如果内容有误,希望大家能帮我指出,非常感谢~
在我的理解中,diff 指代的是 differences
,即 新旧内容之间的区别计算;Vue 中的 diff 算法,则是通过一种 简单且高效 的手段快速对比出 新旧 VNode 节点数组之间的区别 以便 以最少的 dom 操作来更新页面内容。【相关推荐:vuejs视频教程、web前端开发】
此时这里有两个必须的前提:
对比的是 VNode 数组
同时存在新旧两组 VNode 数组
所以它一般只发生在 数据更新造成页面内容需要更新时执行,即 renderWatcher.run()
。
上面说了,diff 中比较的是 VNode,而不是真实的 dom 节点,相信为什么会使用 VNode 大部分人都比较清楚,笔者就简单带过吧?~
在 Vue 中使用 VNode 的原因大致有两个方面:
VNode 作为框架设计者根据框架需求设计的 JavaScript 对象,本身属性相对真实的 dom 节点要简单,并且操作时不需要进行 dom 查询,可以大幅优化计算时的性能消耗
在 VNode 到真实 dom 的这个渲染过程,可以根据不同平台(web、微信小程序)进行不同的处理,生成适配各平台的真实 dom 元素
在 diff 过程中会遍历新旧节点数据进行对比,所以使用 VNode 能带来很大的性能提升。
在网页中,真实的 dom 节点都是以 树 的形式存在的,根节点都是 ,为了保证虚拟节点能与真实 dom 节点一致,VNode 也一样采用的是树形结构。
如果在组件更新时,需要对比全部 VNode 节点的话,新旧两组节点都需要进行 深度遍历 和比较,会产生很大的性能开销;所以,Vue 中默认 同层级节点比较,即 如果新旧 VNode 树的层级不同的话,多余层级的内容会直接新建或者舍弃,只在同层级进行 diff 操作。
一般来说,diff 操作一般发生在 v-for
循环或者有 v-if/v-else
、component
这类 动态生成 的节点对象上(静态节点一般不会改变,对比起来很快),并且这个过程是为了更新 dom,所以在源码中,这个过程对应的方法名是 updateChildren
,位于 src/core/vdom/patch.ts
中。如下图:
这里回顾一下 Vue 组件实例的创建与更新过程:
首先是
beforeCreate
到created
阶段,主要进行数据和状态以及一些基础事件、方法的处理然后,会调用
, quant à la façon dont ce processus à double extrémité commence et se termine, je ne l'ai jamais regardé, j'en ai donc profité pour écrire un article pour l'étudier. soigneusement cette fois. Si le contenu est erroné, j'espère que vous pourrez m'aider à le signaler, merci beaucoup~🎜$mount(vm.$options.el)
方法进入Vnode
与 dom 的创建和挂载阶段,也就是beforeMount
到mounted
un algorithme de comparaison à double extrémité est utilisé🎜Qu'est-ce que la différence ?🎜
🎜D'après ma compréhension, la différence fait référence auxdifférences
, qui est le calcul de la différence entre 🎜ancien et nouveau contenu🎜 ; l'algorithme de comparaison dans Vue compare rapidement la différence entre 🎜anciens et nouveaux tableaux de nœuds VNode via une méthode 🎜simple et efficace🎜 🎜 Pour que 🎜mettre à jour le contenu de la page🎜 avec un minimum d'opérations dom. [Recommandations associées : tutoriel vidéo vuejs, développement web front-end]🎜🎜Il y a deux prérequis nécessaires ici :🎜🎜Donc, généralement, cela ne se produit que lorsque le contenu de la page doit être mis à jour en raison d'une mise à jour des données, c'est-à-dire
- 🎜La comparaison est le tableau VNode🎜
- 🎜Il existe deux ensembles d'anciens et de nouveaux tableaux VNode en même temps🎜
renderWatcher.run()
. 🎜🎜Pourquoi VNode ?🎜
🎜Comme mentionné ci-dessus, ce qui est comparé dans le diff est VNode, pas le vrai nœud dom. Je pense que la plupart des gens utilisent VNode. C'est tout. relativement clair, je vais donc l'expliquer brièvement, n'est-ce pas ?~🎜🎜Il y a environ deux raisons d'utiliser VNode dans Vue : 🎜🎜Pendant le processus de diff , les anciennes et les nouvelles données des nœuds seront parcourues. À titre de comparaison, l'utilisation de VNode peut apporter de grandes améliorations de performances. 🎜
- 🎜VNode as un framework L'objet JavaScript conçu par le concepteur selon les exigences du framework doit avoir des propriétés plus simples que le véritable nœud DOM, et aucune requête DOM n'est requise pendant le fonctionnement, ce qui peut considérablement optimiser la consommation de performances lors du calcul🎜
- 🎜Dans VNode vers le nœud DOM réel Le processus de rendu du dom peut être traité différemment selon les différentes plateformes (web, applet WeChat) pour générer de vrais éléments dom adaptés à chaque plateforme🎜
🎜Tri des processus🎜
🎜Dans la page Web, les vrais nœuds dom existent sous la forme de 🎜tree🎜, et les nœuds racines sont tous< ; html>
, afin de garantir que les nœuds virtuels sont cohérents avec les nœuds dom réels, VNode utilise également une structure arborescente. 🎜🎜Si tous les nœuds VNode doivent être comparés lorsque le composant est mis à jour, les anciens et les nouveaux ensembles de nœuds doivent être 🎜traversés en profondeur🎜 et comparés, ce qui entraînera une surcharge de performances par conséquent, Vue par défaut est 🎜comparer ; nœuds au même niveau🎜, c'est-à-dire 🎜Si les niveaux de l'ancien et du nouveau arbre VNode sont différents, le contenu des niveaux supplémentaires sera directement créé ou supprimé🎜, et seule l'opération de comparaison sera effectuée au même niveau. 🎜🎜De manière générale, les opérations de comparaison se produisent généralement dans des bouclesv-for
ouv-if/v-else
,component
et autres🎜 Dynamiquement généré 🎜 objets nœuds (les nœuds statiques ne changent généralement pas et la comparaison est très rapide), et ce processus consiste à mettre à jour le dom, donc dans le code source, le nom de la méthode correspondant à ce processus estupdateChildren
, Situé danssrc/core/vdom/patch.ts
. Comme indiqué ci-dessous : 🎜🎜🎜
🎜Voici un aperçu du processus de création et de mise à jour des instances de composants Vue :🎜
- 🎜Tout d'abord,
beforeCreate à L'étape <code>created
gère principalement les données et l'état ainsi que certains événements et méthodes de base🎜- 🎜Ensuite,
$mount(vm.$options.el ) entre dans la phase de création et de montage de <code>Vnode
et dom, c'est-à-dire entrebeforeMount
etMounted
( c'est pareil ici lorsque le composant est mis à jour) )🎜Le
$mount
sur le prototype sera réécrit dansplatforms/web/runtime-with-compiler.ts
, et l'implémentation originale est dansplatforms/ web /runtime/index.ts
; dans la méthode d'implémentation d'origine, il appelle en fait la méthodemountComponent
pour exécuterrender
et dansweb ; code> <code>runtime-with-compiler
sous /code> est analysé et compilé une fois, et converti en une fonction liée àoptions.render
$mount
会在platforms/web/runtime-with-compiler.ts
中进行一次重写,原始实现在platforms/web/runtime/index.ts
中;在原始实现方法中,其实就是调用mountComponent
方法执行render
;而在web
下的runtime-with-compiler
中则是增加了 模板字符串编译 模块,会对options
中的的template
进行一次解析和编译,转换成一个函数绑定到options.render
中
mountComponent
函数内部就是 定义了渲染方法updateComponent = () => (vm._update(vm._render())
,实例化一个具有before
配置的watcher
实例(即renderWatcher
),通过定义watch
观察对象为 刚刚定义的updateComponent
方法来执行 首次组件渲染与触发依赖收集,其中的before
配置仅仅配置了触发beforeMount/beforeUpdate
钩子函数的方法;这也是为什么在beforeMount
阶段取不到真实 dom 节点与beforeUpdate
阶段获取的是旧 dom 节点的原因
_update
方法的定义与mountComponent
在同一文件下,其核心就是 读取组件实例中的$el
(旧 dom 节点)与_vnode
(旧 VNode)与_render()
函数生成的vnode
进行patch
操作
patch
函数首先对比 是否具有旧节点,没有的话肯定是新建的组件,直接进行创建和渲染;如果具有旧节点的话,则通过patchVnode
进行新旧节点的对比,并且如果新旧节点一致并且都具有children
子节点,则进入diff
的核心逻辑 ——updateChildren
子节点对比更新,这个方法也是我们常说的diff
算法前置内容
既然是对比新旧 VNode 数组,那么首先肯定有 对比 的判断方法:
sameNode(a, b)
、新增节点的方法addVnodes
、移除节点的方法removeVnodes
,当然,即使sameNode
判断了 VNode 一致之后,依然会使用patchVnode
对单个新旧 VNode 的内容进行深度比较,确认内部数据是否需要更新。sameNode(a, b)
这个方法就一个目的:比较新旧节点是否相同。
在这个方法中,首先比较的就是 a 和 b 的
key
是否相同,这也是为什么 Vue 在文档中注明了v-for、v-if、v-else
等动态节点必须要设置key
来标识节点唯一性,如果key
存在且相同,则只需要比较内部是否发生了改变,一般情况下可以减少很多 dom 操作;而如果没有设置的话,则会直接销毁重建对应的节点元素。然后会比较是不是异步组件,这里会比较他们的构造函数是不是一致。
然后会进入两种不同的情况比较:
- 非异步组件:标签一样、都不是注释节点、都有数据、同类型文本输入框
- 异步组件:旧节点占位符和新节点的错误提示都为
undefined
函数整体过程如下
addVnodes
顾名思义,添加新的 VNode 节点。
该函数接收 6 个参数:
parentElm
当前节点数组父元素、refElm
指定位置的元素、vnodes
新的虚拟节点数组、startIdx
新节点数组的插入元素开始位置、endIdx
新节点数组的插入元素结束索引、insertedVnodeQueue
需要插入的虚拟节点队列。函数内部会 从
startIdx
开始遍历vnodes
数组直到endIdx
位置,然后调用createElm
依次在refElm
之前创建和插入vnodes[idx]
对应的元素。当然,在这个
🎜🎜vnodes[idx]
中有可能会有Component
组件,此时还会调用createComponent
mountComponent À l'intérieur de la fonction est définie la méthode de renduupdateComponent = () => (vm._update(vm._render())
, instancie unwatcher
instance avec la configurationavant
(c'est-à-direrenderWatcher
), en définissant l'objet d'observationwatch
comme la méthodeupdateComponent
qui vient d'être définie pour effectuer le premier rendu des composants et déclencher la collection de dépendances, où la configurationbefore
configure uniquement la méthode pour déclencher la fonction de hookbeforeMount/beforeUpdate
c'est pourquoi le ; Le nœud dom réel etne peuvent pas être obtenus dans l'étape <code>beforeMount
. La raison pour laquelle l'étape code>beforeUpdate obtient l'ancien nœud dom🎜🎜🎜🎜_update est définie dans le même fichier que <code>mountComponent
, et son noyau est Lire le$el
(ancien nœud dom) et_vnode
(ancien VNode) et les fonctions_render()
dans l'instance du composant pour générervnode
effectue une opérationpatch
🎜🎜🎜🎜patch
compare d'abord s'il y a un ancien nœud, sinon, il est sûr qu'il s'agit d'un composant nouvellement créé qui est créé et rendu directement s'il y a d'anciens nœuds, l'ancien ; et les nouveaux nœuds sont comparés viapatchVnode
, et si les anciens et les nouveaux nœuds sont cohérents et que les deux ont un nœud enfantchildren code>, entrez la logique de base de <code>diff
-updateChildren
comparaison et mise à jour des nœuds enfants, cette méthode est aussi ce que nous appelons souventdiff code> Algorithme🎜🎜
sameNode(a, b)
, méthode d'ajout de nœuds addVnodes
, méthode de suppression de nœuds removeVnodes
, bien sûr, même après sameNode détermine que les VNodes sont cohérents, patchVnode
sera toujours utilisé pour comparer en profondeur le contenu d'un seul nouveau et ancien VNode afin de confirmer si les données internes doivent être mises à jour. 🎜clé
de a et b est la même. C'est pourquoi Vue note v-for, v-if, v-else dans. le document
et les autres nœuds dynamiques doivent définir key
pour identifier le caractère unique du nœud. Si key
existe et est identique, il vous suffit de comparer si. des changements internes ont eu lieu. Généralement, cela peut réduire de nombreuses opérations DOM ; s'il n'est pas défini, les éléments de nœud correspondants seront directement détruits et reconstruits. 🎜🎜Ensuite, il comparera s'il s'agit d'un composant asynchrone, et ici il comparera si leurs constructeurs sont cohérents. 🎜🎜Ensuite, vous entrerez deux situations différentes à des fins de comparaison : 🎜non définis
🎜parentElm
l'élément parent du tableau de nœuds actuel, refElm
l'élément à la position spécifiée, vnodes
nouveau tableau de nœuds virtuels, startIdx
La position de départ de l'élément inséré du nouveau tableau de nœuds, endIdx
L'index de fin de l'élément inséré du nouveau nœud array, insertedVnodeQueue
obligatoire La file d'attente des nœuds virtuels insérés. 🎜🎜En interne, la fonction parcourra le tableau vnodes
à partir de startIdx
jusqu'à la position endIdx
, puis appellera createElm Créez et insérez tour à tour les éléments correspondant à <code>vnodes[idx]
avant refElm
. 🎜🎜Bien sûr, il peut y avoir un composant Component
dans ce vnodes[idx]
, et createComponent
sera également appelé pour créer le composant correspondant . Exemple. 🎜Parce que l'ensemble du
VNode
et du dom sont uneVNode
和 dom 都是一个 树结构,所以在 同层级的比较之后,还需要处理当前层级下更深层次的 VNode 和 dom 处理。
与 addVnodes
相反,该方法就是用来移除 VNode 节点的。
由于这个方法只是移除,所以只需要三个参数:vnodes
旧虚拟节点数组、startIdx
开始索引、endIdx
结束索引。
函数内部会 从 startIdx
开始遍历 vnodes
数组直到 endIdx
位置,如果 vnodes[idx]
不为 undefined
的话,则会根据 tag
属性来区分处理:
tag
,说明是一个元素或者组件,需要 递归处理 vnodes[idx]
的内容, 触发 remove hooks 与 destroy hooks
tag
,说明是一个 纯文本节点,直接从 dom 中移除该节点即可节点对比的 实际完整对比和 dom 更新 方法。
在这个方法中,主要包含 九个 主要的参数判断,并对应不同的处理逻辑:
新旧 VNode 全等,则说明没有变化,直接退出
如果新的 VNode 具有真实的 dom 绑定,并且需要更新的节点集合是一个数组的话,则拷贝当前的 VNode 到集合的指定位置
如果旧节点是一个 异步组件并且还没有加载结束的话就直接退出,否则通过 hydrate
函数将新的 VNode 转化为真实的 dom 进行渲染;两种情况都会 退出该函数
如果新旧节点都是 静态节点 并且 key
相等,或者是 isOnce
指定的不更新节点,也会直接 复用旧节点的组件实例 并 退出函数
如果新的 VNode 节点具有 data
属性并且有配置 prepatch
钩子函数,则执行 prepatch(oldVnode, vnode)
通知进入节点的对比阶段,一般这一步会配置性能优化
如果新的 VNode 具有 data
属性并且递归改节点的子组件实例的 vnode,依然是可用标签的话,cbs
回调函数对象中配置的 update
钩子函数以及 data
中配置的 update
钩子函数
如果新的 VNode 不是文本节点的话,会进入核心对比阶段:
children
子节点,则进入 updateChildren
方法对比子节点text
文本如果新的 VNode 具有 text
文本(是文本节点),则比较新旧节点的文本内容是否一致,否则进行文本内容的更新
最后调用新节点的 data
中配置的 postpatch
钩子函数,通知节点更新完毕
简单来说,patchVnode
就是在 同一个节点 更新阶段 进行新内容与旧内容的对比,如果发生改变则更新对应的内容;如果有子节点,则“递归”执行每个子节点的比较和更新。
而 子节点数组的比较和更新,则是 diff 的核心逻辑,也是面试时经常被提及的问题之一。
下面,就进入 updateChildren
方法的解析吧~
updateChildren
structure arborescente, donc après une . removeVnodes
Contrairement à addVnodes
, cette méthode est utilisée pour supprimer les nœuds VNode. Comme cette méthode supprime uniquement, elle ne nécessite que trois paramètres : vnodes
Ancien tableau de nœuds virtuels, startIdx
start index, endIdx
end index .
La fonction interne traversera le tableau vnodes
à partir de startIdx
jusqu'à la position endIdx
, si vnodes[idx] S'il n'est pas <code>indéfini
, il sera traité selon l'attribut tag
:
tag
indique qu'il s'agit d'un élément ou d'un composant qui doit traiter de manière récursive le contenu de vnodes[idx]
et déclencher la supprimer les hooks et détruire les hooks
tag
n'existe pas >, la description est un 🎜nœud de texte brut🎜, supprimez simplement le nœud directement du domhydrate
; Dans les deux cas, la fonction 🎜quitter🎜🎜clé code> est égal, ou <code>isOnce
est spécifié. Si le nœud n'est pas mis à jour, il 🎜réutilisera directement l'instance de composant de l'ancien nœud🎜 et 🎜quittera la fonction🎜🎜
data
et a la fonction de hook de configurationprepatch
, alors exécutez prepatch(oldVnode, vnode)
pour notifier le nœud pour entrer dans la phase de comparaison. Généralement, cette étape configurera l'optimisation des performances🎜data
et modifie récursivement le vnode de l'instance de sous-composant du nœud If. le label est toujours disponible, la update
configurée dans l'objet fonction de rappel cbs
>Fonction Hook et la fonction hook update
configurée dans data🎜
enfants
nœuds enfants, entrez la méthode updateChildren
pour comparer les nœuds enfantstext
texttext
Text (c'est un nœud de texte), comparez le contenu textuel de l'ancien et le nouveau nœuds pour voir s'il est cohérent, sinon mettez à jour le contenu du texte🎜 configuré dans les <code>data
>postpatchdu nouveau nœud > fonction hook pour avertir le nœud que la mise à jour est terminée🎜patchVnode
consiste à traiter le nouveau contenu et l'ancien contenu dans la même étape de mise à jour du nœud🎜 Par comparaison , s'il y a un changement, le contenu correspondant est mis à jour ; s'il y a des nœuds enfants, la comparaison et la mise à jour de chaque nœud enfant sont effectuées "récursivement"🎜. 🎜🎜Et 🎜la comparaison et la mise à jour des tableaux de nœuds enfants sont la logique fondamentale de diff🎜, et c'est aussi l'une des questions souvent mentionnées lors des entretiens. 🎜🎜Maintenant, entrons dans l'analyse de la méthode updateChildren
~🎜updateChildren
analyse de base diff🎜🎜🎜Tout d'abord, Nous y réfléchissons d'abord🎜Quelles sont les méthodes pour comparer la différence entre les éléments de deux tableaux d'objets basés sur le nouveau tableau🎜 ? 🎜🎜De manière générale, nous pouvons parcourir directement deux tableaux🎜 grâce à la 🎜force brute pour trouver l'ordre et la différence de chaque élément du tableau, qui est l'🎜algorithme de comparaison simple🎜. 🎜🎜C'est-à-dire 🎜 parcourez le nouveau tableau de nœuds, parcourez à nouveau l'ancien tableau de nœuds à chaque cycle pour comparer si les deux nœuds sont cohérents et déterminez si le nouveau nœud est ajouté, supprimé ou déplacé dans l'ensemble du processus de comparaison. nécessite une comparaison m*n, donc la complexité temporelle par défaut est On. 🎜Cette méthode de comparaison consomme beaucoup de performances lors du processus de mise à jour d'un grand nombre de nœuds, c'est pourquoi Vue 2 l'a optimisée et l'a modifiée en algorithme de comparaison double
, c'est-à-dire double -terminé diff
code>. 双端对比算法
,也就是 双端 diff
。
顾名思义,双端 就是 从两端开始分别向中间进行遍历对比 的算法。
在 双端 diff
中,分为 五种比较情况:
新旧头相等
新旧尾相等
旧头等于新尾
旧尾等于新头
四者互不相等
其中,前四种属于 比较理想的情况,而第五种才是 最复杂的对比情况。
判断相等即
sameVnode(a, b)
等于true
下面我们通过一种预设情况来进行分析。
为了尽量同时演示出以上五种情况,我预设了以下的新旧节点数组:
oldChildren
,包含 1 - 7 共 7 个节点newChildren
,也有 7 个节点,但是相比旧节点减少了一个 vnode 3
并增加了一个 vnode 8
在进行比较之前,首先需要 定义两组节点的双端索引:
let oldStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newStartIdx = 0 let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx]
复制的源代码,其中
oldCh
在图中为oldChildren
,newCh
为newChildren
然后,我们定义 遍历对比操作的停止条件:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
这里的停止条件是 只要新旧节点数组任意一个遍历结束,则立即停止遍历。
此时节点状态如下:
为了保证新旧节点数组在对比时不会进行无效对比,会首先排除掉旧节点数组 起始部分与末尾部分 连续且值为 Undefined
的数据。
if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]
当然我们的例子中没有这种情况,可以忽略。
此时相当于新旧节点数组的两个 起始索引 指向的节点是 基本一致的,那么此时会调用 patchVnode
对两个 vnode 进行深层比较和 dom 更新,并且将 两个起始索引向后移动。即:
if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode( oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
这时的节点和索引变化如图所示:
与头结点相等类似,这种情况代表 新旧节点数组的最后一个节点基本一致,此时一样调用 patchVnode
比较两个尾结点和更新 dom,然后将 两个末尾索引向前移动。
if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode( oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx ) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
这时的节点和索引变化如图所示:
这里表示的是 旧节点数组 当前起始索引 指向的 vnode 与 新节点数组 当前末尾索引 指向的 vnode 基本一致,一样调用 patchVnode
对两个节点进行处理。
但是与上面两种有区别的地方在于:这种情况下会造成 节点的移动,所以此时还会在 patchVnode
结束之后 通过 nodeOps.insertBefore
将 旧的头节点 重新插入到 当前 旧的尾结点之后。
然后,会将 旧节点的起始索引后移、新节点的末尾索引前移。
dans la figure,看到这里大家可能会有一个疑问,为什么这里移动的是 旧的节点数组,这里因为 vnode 节点中有一个属性
elm
Algorithme de comparaison à double extrémité
Comme son nom l'indique, Double-ended est un algorithme qui part des deux extrémités et traverse jusqu'au milieu🎜. 🎜🎜Dansdiff double-ended
, il est divisé en 🎜cinq situations de comparaison🎜 : 🎜🎜Parmi eux, les quatre premiers sont des 🎜situations idéales🎜, tandis que la cinquième est la 🎜la plus compliquée situation de comparaison🎜. 🎜
- 🎜Les anciens et les nouveaux en-têtes sont égales🎜 li>
- 🎜L'ancienne et la nouvelle queue sont égales🎜
- 🎜L'ancienne tête est égale à la nouvelle queue🎜
- 🎜L'ancienne queue est égale au nouveau chef🎜
- 🎜Les quatre s'excluent mutuellement Égalité🎜
🎜Le jugement d'égalité signifie que🎜Analysons-le à travers une situation prédéfinie. 🎜sameVnode(a, b)
est égal àtrue
🎜🎜1. Prérégler les nouveaux et anciens états de nœuds🎜
🎜Afin de démontrer les cinq situations ci-dessus en même temps, j'ai prédéfini les nouveaux et anciens tableaux de nœuds suivants. : 🎜
- Comme l'ancien tableau de nœuds
oldChildren
dans l'ordre initial des nœuds, comprenant 1 à 7 au total 7 nœuds- Comme le nouveau tableau de nœuds
🎜sont comparés. Avant, vous devez d'abord 🎜définir l'index double 🎜 des deux ensembles de nœuds : 🎜newChildren
après le code aléatoire>, il y a aussi 7 nœuds, mais par rapport à l'ancien nœud, unvnode 3
de moins et unvnode 8 de plus
if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode( oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx ) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }🎜Code source copié, oùoldCh
est <. code>oldChildren
newCh
est newChildren
🎜🎜Ensuite, nous définissons la condition d'arrêt🎜 de l'opération de comparaison de traversée🎜 :🎜 if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode( oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }🎜La condition d'arrêt ici est🎜tant que l'un des anciens et des nouveaux tableaux de nœuds est traversé. Lorsqu'il se termine, le parcours s'arrêtera immédiatement🎜. 🎜🎜L'état du nœud à l'heure actuelle est le suivant : 🎜🎜
Undéfini
. 🎜if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode( vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore( parentElm, vnodeToMove.elm, oldStartVnode.elm ) } else { // same key but different element. treat as new element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } } newStartVnode = newCh[++newStartIdx] }🎜
patchVnode
sera appelé à ce moment pour effectuer une comparaison approfondie et une mise à jour dom des deux nœuds virtuels, et déplacer les 🎜deux index de départ vers l'arrière 🎜. . C'est-à-dire : 🎜rrreee🎜Les changements de nœud et d'index à ce moment sont comme indiqué dans la figure : 🎜🎜patchVnode
est également appelé pour comparer les deux queues. nœuds et mettez à jour le dom, puis avancez les deux index de queue. 🎜rrreee🎜Les changements de nœud et d'index à ce moment sont comme indiqué dans la figure : 🎜🎜patchVnode
pour traiter les deux nœuds. 🎜🎜Mais la différence avec les deux ci-dessus est que dans ce cas, le 🎜nœud sera déplacé🎜, donc à ce moment-là, il passera également nodeOps.insertAvant après la fin de 🎜<code>patchVnode
🎜 Réinsérez l'🎜ancien nœud de tête🎜 après l'🎜ancien nœud de queue actuel🎜. 🎜🎜Ensuite, l'index de départ de l'ancien nœud sera déplacé vers l'arrière et l'index de fin du nouveau nœud sera déplacé vers l'avant. 🎜🎜Vous aurez peut-être une question lorsque vous verrez ceci, pourquoi l'ancien tableau de nœuds🎜 est-il déplacé ici ? C'est parce qu'il y a un attribut elm
dans le nœud vnode, qui pointera vers le vnode correspondant Le nœud dom réel, donc déplacer l'ancien tableau de nœuds ici revient en fait à 🎜déplacer l'ordre réel des nœuds dom depuis le côté🎜 ; et notez qu'il s'agit du 🎜nœud de queue actuel. Une fois l'index modifié, ce n'est pas nécessairement le cas. la fin de l'ancien tableau de nœuds d'origine 🎜. 🎜
即:
if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode( oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx ) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }
此时状态如下:
这里与上面的 旧头等于新尾 类似,一样要涉及到节点对比和移动,只是调整的索引不同。此时 旧节点的 末尾索引 前移、新节点的 起始索引 后移,当然了,这里的 dom 移动对应的 vnode 操作是 将旧节点数组的末尾索引对应的 vnode 插入到旧节点数组 起始索引对应的 vnode 之前。
if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode( oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }
此时状态如下:
在以上情况都处理之后,就来到了四个节点互相都不相等的情况,这种情况也是 最复杂的情况。
当经过了上面几种处理之后,此时的 索引与对应的 vnode 状态如下:
可以看到四个索引对应的 vnode 分别是:vnode 3、vnode 5、 vnode 4、vnode 8,这几个肯定是不一样的。
此时也就意味着 双端对比结束。
后面的节点对比则是 将旧节点数组剩余的 vnode (oldStartIdx
到 oldEndIdx
之间的节点)进行一次遍历,生成由 vnode.key
作为键,idx
索引作为值的对象 oldKeyToIdx
,然后 遍历新节点数组的剩余 vnode(newStartIdx
到 newEndIdx
之间的节点),根据新的节点的 key
在 oldKeyToIdx
进行查找。此时的每个新节点的查找结果只有两种情况:
找到了对应的索引,那么会通过 sameVNode
对两个节点进行对比:
patchVnode
进行深层对比和 dom 更新,将 oldKeyToIdx
中对应的索引 idxInOld
对应的节点插入到 oldStartIdx
对应的 vnode 之前;并且,这里会将 旧节点数组中 idxInOld
对应的元素设置为 undefined
createElm
重新创建一个新的 dom 节点并将 新的 vnode 插入到对应的位置
没有找到对应的索引,则直接 createElm
创建新的 dom 节点并将新的 vnode 插入到对应位置
注:这里 只有找到了旧节点并且新旧节点一样才会将旧节点数组中
idxInOld
中的元素置为undefined
。
最后,会将 新节点数组的 起始索引 向后移动。
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode( vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore( parentElm, vnodeToMove.elm, oldStartVnode.elm ) } else { // same key but different element. treat as new element createElm( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx ) } } newStartVnode = newCh[++newStartIdx] }
大致逻辑如下图:
经过上面的处理之后,根据判断条件也不难看出,遍历结束之后 新旧节点数组都刚好没有剩余元素 是很难出现的,当且仅当遍历过程中每次新头尾节点总能和旧头尾节点中总能有两个新旧节点相同时才会发生,只要有一个节点发生改变或者顺序发生大幅调整,最后 都会有一个节点数组起始索引和末尾索引无法闭合。
那么此时就需要对剩余元素进行处理:
即:
Vue 2 的 diff 算法相对于简单 diff 算法来说,通过 双端对比与生成索引 map 两种方式 减少了简单算法中的多次循环操作,新旧数组均只需要进行一次遍历即可将所有节点进行对比。
La comparaison à double extrémité sera comparée et déplacée quatre fois respectivement, et les performances ne sont pas la solution optimale. Par conséquent, Vue 3 a introduit la Sous-séquence croissante la plus longue pour remplacer la comparaison à double extrémité, tandis que le reste passe toujours. La conversion sous forme de carte d'index utilise l'expansion de l'espace pour réduire la complexité temporelle, améliorant ainsi encore les performances informatiques.
Bien sûr, le nœud dom réel d'elm correspondant à vnode n'est pas illustré dans la figure de cet article. La relation mobile entre les deux peut provoquer des malentendus. Il est recommandé de le lire avec "Conception et implémentation de Vue.js". .
Le processus global est le suivant :
(Partage de vidéos d'apprentissage : Tutoriel d'introduction à vuejs, Vidéo de programmation de base)
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!