Introduction
Depuis deux ans que j'utilise Vue pour mes projets, je maîtrise bien ses API. Bien que j'aie entendu parler de certains principes de fonctionnement comme le DOM virtuel, flow, le pilotage par les données, ou le fonctionnement du routage, je n'ai jamais vraiment approfondi ces fondements ni examiné comment le code source de Vue met en œuvre ces concepts. C'est pourquoi j'ai entrepris de structurer les principes techniques du framework Vue et son implémentation spécifique. Si vous êtes intéressé par les principes de fonctionnement de Vue, cet article vous ouvrira les portes de son monde sous-jacent pour explorer ses détails d'implémentation. Cet article se concentre sur les principes techniques du DOM virtuel et son implémentation dans le framework Vue.
Un. Le vrai DOM et son processus de parsing
Dans cette section, nous présentons le processus de parsing du vrai DOM. En expliquant ce processus et ses problèmes, nous justifierons pourquoi le DOM virtuel est nécessaire. Une image vaut mille mots, comme le montre le diagramme ci-dessous du fonctionnement du moteur de rendu Webkit :
Le processus de travail de tous les moteurs de rendu de navigateur se divise approximativement en 5 étapes : création de l'arbre DOM → création des règles de style → construction de l'arbre de rendu → mise en page (Layout) → dessin (Painting).
- Première étape : construction de l'arbre DOM : l'analyseur HTML analyse les éléments HTML et construit un arbre DOM ;
- Deuxième étape : génération des feuilles de style : l'analyseur CSS analyse les fichiers CSS et les styles en ligne des éléments pour générer les feuilles de style de la page ;
- Troisième étape : construction de l'arbre de rendu : l'arbre DOM et les feuilles de style sont associés pour construire un arbre de rendu. Chaque nœud DOM a une méthode attach qui accepte les informations de style et retourne un objet de rendu. Ces objets de rendu sont finalement assemblés en un arbre de rendu ;
- Quatrième étape : détermination des coordonnées des nœuds : selon la structure de l'arbre de rendu, des coordonnées précises sont déterminées pour chaque nœud sur l'écran ;
- Cinquième étape : dessin de la page : selon l'arbre de rendu et les coordonnées d'affichage, la méthode paint de chaque nœud est appelée pour les dessiner.
Points importants :
1. La construction de l'arbre DOM commence-t- seulement après le chargement complet du document ? La construction de l'arbre DOM est un processus progressif. Pour offrir une meilleure expérience utilisateur, le moteur de rendu affiche le contenu sur l'écran dès que possible, sans attendre la fin complète de l'analyse du document HTML.
2. L'arbre de rendu est-il construit seulement après la fin de la construction de l'arbre DOM et des feuilles de style ? Ces trois processus ne sont pas totalement indépendants dans leur exécution, mais se chevauchent. Le navigateur charge, analyse et rend simultanément.
3. Points à considérer pour l'analyse CSS ? L'analyse CSS se fait de droite à gauche. Plus les balises sont imbriquées, plus l'analyse est lente.
4. Quel est le coût des opérations JS sur le vrai DOM ? Avec le modèle de développement traditionnel, lorsque nous utilisons du JavaScript natif ou jQuery pour manipuler le DOM, le navigateur exécute tout le processus depuis le début. Si dans une opération nous devons mettre à jour 10 nœuds DOM, le navigateur, après avoir reçu la première requête, ignore les 9 mises à jour suivantes et exécute immédiatement le processus, ce qui entraîne 10 exécutions. Par exemple, après le premier calcul, si une nouvelle requête de mise à jour du DOM arrive, les coordonnées du nœud changent, rendant le calcul précédent inutile. Le calcul des coordonnées des nœuds DOM est un gaspillage de performances. Même si le matériel informatique évolue constamment, le coût des opérations DOM reste élevé, et les manipulations fréquentes peuvent provoquer un ralentissement de la page, affectant l'expérience utilisateur.
Deux. Fondements du DOM virtuel
2.1. Avantages du DOM virtuel
Le DOM virtuel a été conçu pour résoudre les problèmes de performance des navigateurs. Comme mentionné précédemment, si une opération comporte 10 mises à jour du DOM, le DOM virtuel n'effectuera pas ces mises à jour immédiatement. Il sauvegardera plutôt le contenu des différences (diff) de ces 10 mises à jour dans un objet JavaScript local, puis appliquera cet objet en une seule fois à l'arbre DOM avant d'effectuer d'autres opérations, évitant ainsi de nombreuses calculs inutiles. L'avantage de simuler les nœuds DOM avec des objets JavaScript est que les mises à jour de la page peuvent d'abord être reflétées dans l'objet JavaScript (DOM virtuel), et manipuler des objets JavaScript en mémoire est évidemment plus rapide. Une fois les mises à jour terminées, l'objet JavaScript final est mappé au vrai DOM pour que le navigateur puisse l'afficher.
2.2. Implémentation algorithmique
2.2.1. Simuler un arbre DOM avec des objets JavaScript
(1) Comment simuler un arbre DOM avec des objets JavaScript
Par exemple, un vrai nœud DOM ressemble à ceci :
<div id="dom-virtuel">
<p>DOM Virtuel</p>
- Élément 1
- Élément 2
- Élément 3
<div>Bonjour le monde</div>
</div>
Nous pouvons représenter un nœud DOM avec un objet JavaScript, en utilisant les propriétés de l'objet pour enregistrer le type de nœud, ses attributs, ses nœuds enfants, etc.
Le code dans element.js pour représenter l'objet nœud est le suivant :
/**
* Définition de l'objet virtuel DOM Element
* @param {String} tagName - nom de l'élément DOM
* @param {Object} props - attributs DOM
* @param {Array<Element|String>} - nœuds enfants
*/
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
// Valeur clé de l'élément DOM, utilisée comme identifiant unique
if(props.key){
this.key = props.key
}
var count = 0
children.forEach(function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
// Nombre d'éléments enfants
this.count = count
}
function creerElement(tagName, props, children){
return new Element(tagName, props, children);
}
module.exports = creerElement;
Selon la définition de l'objet element, la structure DOM ci-dessus peut être simplement représentée par :
var el = require("./element.js");
var ul = el('div',{id:'dom-virtuel'},[
el('p',{},['DOM Virtuel']),
el('ul', { id: 'liste' }, [
el('li', { class: 'element' }, ['Élément 1']),
el('li', { class: 'element' }, ['Élément 2']),
el('li', { class: 'element' }, ['Élément 3'])
]),
el('div',{},['Bonjour le monde'])
])
Maintenant, ul est la structure DOM que nous représentons avec un objet JavaScript. Si nous examinons la structure de données correspondant à ul, nous obtenons :
(2) Rendu de l'objet DOM représenté en JavaScript
Mais la page n'a pas cette structure. Nous allons maintenant expliquer comment rendre ul en une vraie structure DOM sur la page. La fonction de rendu correspondante est la suivante :
/**
* render rend un objet virtuel DOM en élément DOM réel
*/
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
// Définition des attributs DOM du nœud
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // Si le nœud enfant est aussi un DOM virtuel, construction récursive du nœud DOM
: document.createTextNode(child) // Si c'est une chaîne, construction d'un nœud texte uniquement
el.appendChild(childEl)
})
return el
}
Nous pouvons voir dans la méthode render ci-dessus qu'un véritable nœud DOM est construit en fonction de tagName, puis les attributs de ce nœud sont définis, et finalement ses nœuds enfants sont construits récursivement.
Nous ajoutons la structure DOM construite à la page body comme suit :
ulRacine = ul.render();
document.body.appendChild(ulRacine);
Ainsi, la page body contient une vraie structure DOM, comme le montre l'effet ci-dessous :
2.2.2. Comparaison de deux arbres DOM virtuels — algorithme diff
L'algorithme diff est utilisé pour comparer les différences entre deux arbres DOM virtuels. Si une comparaison complète des deux arbres est nécessaire, la complexité temporelle de l'algorithme diff serait de O(n^3). Cependant, dans le développement frontend, il est rare de déplacer des éléments DOM entre différents niveaux. Par conséquent, le DOM virtuel ne compare que les éléments du même niveau, comme indiqué dans la figure ci-dessous. Le div ne sera comparé qu'avec d'autres div au même niveau, le deuxième niveau ne sera comparé qu'avec d'autres éléments du deuxième niveau, etc. Ainsi, la complexité de l'algorithme peut être réduite à O(n).
- Parcours en profondeur d'abord, enregistrement des différences
Dans le code réel, les deux arbres (ancien et nouveau) sont parcourus en profondeur d'abord, de sorte que chaque nœud reçoit un marquage unique :
Lors du parcours en profondeur d'abord, chaque nœud est comparé avec le nouvel arbre. S'il y a des différences, elles sont enregistrées dans un objet.
// fonction diff, comparaison de deux arbres
function diff(arbreAncien, arbreNouveau) {
var index = 0 // marqueur du nœud actuel
var patches = {} // objet utilisé pour enregistrer les différences de chaque nœud
parcoursProfondeur(arbreAncien, arbreNouveau, index, patches)
return patches
}
// Parcours en profondeur d'abord des deux arbres
function parcoursProfondeur(noeudAncien, noeudNouveau, index, patches) {
var correctifActuel = []
if (typeof (noeudAncien) === "string" && typeof (noeudNouveau) === "string") {
// Changement de contenu texte
if (noeudNouveau !== noeudAncien) {
correctifActuel.push({ type: correctif.TEXTE, contenu: noeudNouveau })
}
} else if (noeudNouveau!=null && noeudAncien.tagName === noeudNouveau.tagName && noeudAncien.key === noeudNouveau.key) {
// Nœuds identiques, comparaison des attributs
var propsCorrectifs = diffProps(noeudAncien, noeudNouveau)
if (propsCorrectifs) {
correctifActuel.push({ type: correctif.PROPS, props: propsCorrectifs })
}
// Comparaison des nœuds enfants, si les nœuds enfants ont la propriété 'ignore', pas besoin de comparer
if (!estIgnorerEnfants(noeudNouveau)) {
diffEnfants(
noeudAncien.children,
noeudNouveau.children,
index,
patches,
correctifActuel
)
}
} else if(noeudNouveau !== null){
// Le nouveau nœud est différent de l'ancien, on le remplace
correctifActuel.push({ type: correctif.REMPLACER, noeud: noeudNouveau })
}
if (correctifActuel.length) {
patches[index] = correctifActuel
}
}
De ce qui précède, on peut déduire que patches[1] représente p, patches[3] représente ul, et ainsi de suite.
(2) Types de différences
Les types de différences causées par les opérations DOM incluent les suivants :
- Remplacement de nœud : le nœud a changé, par exemple remplacer le
divpar unh1; - Réorganisation de l'ordre : déplacement, suppression, ajout de nœuds enfants, par exemple échanger l'ordre des
petuldans ledivci-dessus; - Modification d'attributs : modification des attributs du nœud, par exemple suppression de la classe de style
classdulici-dessus; - Changement de texte : modification du contenu du nœud texte, par exemple modification du contenu du nœud
pen "DOM Réel";
Ces types de différences sont définis dans le code comme suit :
var REMPLACER = 0 // Remplacer le nœud original
var REORDONNER = 1 // Réorganiser l'ordre
var PROPS = 2 // Modification des attributs du nœud
var TEXTE = 3 // Changement du contenu du texte
(3) Algorithme de comparaison de listes
L'algorithme de comparaison des nœuds enfants, par exemple l'ordre de p, ul, div est changé en div, p, ul. Comment comparer cela ? Si nous comparons dans le même niveau selon l'ordre, tous seraient remplacés. Par exemple, p et div ont des tagName différents, p serait remplacé par div. Finalement, les trois nœuds seraient remplacés, ce qui entraînerait un coût DOM très important. En réalité, il n'est pas nécessaire de remplacer les nœuds, il suffit de les déplacer. Nous devons simplement savoir comment effectuer ces déplacements.
Ce problème peut être abstrait comme le problème de la distance d'édition minimale entre deux chaînes de caractères (Distance d'édition). La méthode la plus courante pour résoudre ce problème est la distance de Levenshtein. La distance de Levenshtein est une mesure de la différence entre deux séquences de caractères. La distance de Levenshtein entre deux mots est le nombre minimum d'opérations sur des caractères uniques (insertion, suppression ou substitution) nécessaires pour convertir un mot en un autre. La distance de Levenshtein a été inventée en 1965 par le mathématicien soviétique Vladimir Levenshtein. La distance de Levenshtein est également connue sous le nom de distance d'édition (Edit Distance), résolue par programmation dynamique, avec une complexité temporelle de O(M*N).
Définition : Pour deux chaînes de caractères a, b, leur distance de Levenshtein est :
Exemple : pour les chaînes de caractères a et b, a="abcde" , b="cabef", selon la formule de calcul ci-dessus, le processus de calcul de leur distance de Levenshtein est le suivant :
Dans notre démo, nous utilisons le plugin liste-diff2 pour effectuer la comparaison, dont la complexité temporelle est de O(n*m). Bien que cet algorithme ne soit pas optimal, il est suffisant pour les opérations DOM courantes. Le processus d'implémentation spécifique de cet algorithme n'est pas détaillé ici. Pour plus d'informations, vous pouvez vous référer à : github.com/livoras/list-diff2
(4) Exemple de sortie
Deux objets DOM virtuels sont illustrés ci-dessous, où ul1 représente l'arbre DOM virtuel d'origine, et ul2 représente l'arbre DOM virtuel modifié.
var ul1 = el('div',{id:'dom-virtuel'},[
el('p',{},['DOM Virtuel']),
el('ul', { id: 'liste' }, [
el('li', { class: 'element' }, ['Élément 1']),
el('li', { class: 'element' }, ['Élément 2']),
el('li', { class: 'element' }, ['Élément 3'])
]),
el('div',{},['Bonjour le monde'])
])
var ul2 = el('div',{id:'dom-virtuel'},[
el('p',{},['DOM Virtuel']),
el('ul', { id: 'liste' }, [
el('li', { class: 'element' }, ['Élément 21']),
el('li', { class: 'element' }, ['Élément 23'])
]),
el('p',{},['Bonjour le monde'])
])
var correctifs = diff(ul1,ul2);
console.log('correctifs:',correctifs);
Nous examinons l'objet de différence entre les deux objets DOM virtuels, comme indiqué ci-dessous. Nous pouvons comprendre par cet objet de différence quelles modifications ont été apportées entre les deux objets DOM virtuels, et ainsi appliquer cet objet de différence (correctifs) à la structure DOM d'origine pour modifier la structure DOM de la page.
2.2.3. Application des différences entre deux objets DOM virtuels au véritable arbre DOM
(1) Parcours en profondeur d'abord de l'arbre DOM
Comme l'objet arbre JavaScript construit à l'étape 1 et l'arbre DOM réel rendu ont les mêmes informations et structure, nous pouvons également parcourir en profondeur d'abord cet arbre DOM. Pendant le parcours, nous trouvons les différences du nœud actuel dans l'objet patches généré à l'étape 2, comme le montre le code correspondant ci-dessous :
function corriger (noeud, correctifs) {
var parcours = {index: 0}
parcoursProfondeur(noeud, parcours, correctifs)
}
function parcoursProfondeur (noeud, parcours, correctifs) {
// Récupération des différences du nœud actuel depuis les correctifs
var correctifsActuels = correctifs[parcours.index]
var longueur = noeud.childNodes
? noeud.childNodes.length
: 0
// Parcours en profondeur des nœuds enfants
for (var i = 0; i < longueur; i++) {
var enfant = noeud.childNodes[i]
parcours.index++
parcoursProfondeur(enfant, parcours, correctifs)
}
// Opérations DOM sur le nœud actuel
if (correctifsActuels) {
appliquerCorrectifs(noeud, correctifsActuels)
}
}
(2) Opérations DOM sur l'arbre DOM existant
Nous effectuons différentes opérations DOM sur le nœud actuel en fonction des différents types de différences. Par exemple, si un remplacement de nœud est effectué, nous procédons à une opération de remplacement de nœud ; si le texte du nœud a changé, nous procédons à une opération de remplacement de texte ; ainsi que des opérations de réorganisation des nœuds enfants, de modification des attributs, etc. Le code correspondant est montré dans appliquerCorrectifs :
function appliquerCorrectifs (noeud, correctifsActuels) {
correctifsActuels.forEach(correctifActuel => {
switch (correctifActuel.type) {
case REMPLACER:
var nouveauNoeud = (typeof correctifActuel.noeud === 'string')
? document.createTextNode(correctifActuel.noeud)
: correctifActuel.noeud.render()
noeud.parentNode.replaceChild(nouveauNoeud, noeud)
break
case REORDONNER:
reordonnerEnfants(noeud, correctifActuel.deplacements)
break
case PROPS:
definirProps(noeud, correctifActuel.props)
break
case TEXTE:
noeud.textContent = correctifActuel.contenu
break
default:
throw new Error('Type de correctif inconnu ' + correctifActuel.type)
}
})
}
(3) Modification de la structure DOM
En appliquant les différences entre les deux objets DOM de la section 2.2.2 à la première structure DOM (d'origine), nous pouvons voir que la structure DOM a subi les modifications attendues, comme illustré ci-dessous :
2.3. Conclusion
L'algorithme du DOM virtuel met principalement en œuvre les trois étapes ci-dessus :
- Simulation de l'arbre DOM avec des objets JavaScript —
element.js
<div id="dom-virtuel">
<p>DOM Virtuel</p>
- Élément 1
- Élément 2
- Élément 3
<div>Bonjour le monde</div>
</div>
- Comparaison des différences entre deux arbres DOM virtuels —
diff.js
Application des différences entre deux objets DOM virtuels au véritable arbre DOM — patch.js
function appliquerCorrectifs (noeud, correctifsActuels) {
correctifsActuels.forEach(correctifActuel => {
switch (correctifActuel.type) {
case REMPLACER:
var nouveauNoeud = (typeof correctifActuel.noeud === 'string')
? document.createTextNode(correctifActuel.noeud)
: correctifActuel.noeud.render()
noeud.parentNode.replaceChild(nouveauNoeud, noeud)
break
case REORDONNER:
reordonnerEnfants(noeud, correctifActuel.deplacements)
break
case PROPS:
definirProps(noeud, correctifActuel.props)
break
case TEXTE:
noeud.textContent = correctifActuel.contenu
break
default:
throw new Error('Type de correctif inconnu ' + correctifActuel.type)
}
})
}
Trois. Analyse simplifiée du DOM virtuel dans le code source de Vue
Dans le chapitre deux (Fondements du DOM virtuel), nous avons appris que le rendu du DOM virtuel en un vrai DOM implique en réalité les processus de définition de VNode, diff, patch, etc. Ainsi, l'analyse du code source de Vue dans ce chapitre suit également ces processus.
3.1. Simulation de l'arbre DOM avec VNode
3.1.1. Brève analyse de la classe VNode
Dans Vue.js, le DOM virtuel est décrit par une classe VNode, définie dans src/core/vdom/vnode.js. On peut voir dans le bloc de code ci-dessous que la définition du DOM virtuel dans Vue.js est plus complexe car elle intègre de nombreuses fonctionnalités spécifiques de Vue.js. En réalité, le DOM virtuel dans Vue.js s'inspire fortement d'une bibliothèque open source nommée snabbdom, à laquelle ont été ajoutées certaines fonctionnalités spécifiques à Vue.js.
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendu dans le scope de ce composant
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // instance du composant
parent: VNode | void; // nœud placeholder du composant
// strictement interne
raw: boolean; // contient du HTML brut ? (serveur uniquement)
isStatic: boolean; // nœud statique hissé
isRootInsert: boolean; // nécessaire pour la vérification de transition d'entrée
isComment: boolean; // placeholder de commentaire vide ?
isCloned: boolean; // est un nœud cloné ?
isOnce: boolean; // est un nœud v-once ?
asyncFactory: Function | void; // fonction factory du composant asynchrone
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // vrai contexte vm pour les nœuds fonctionnels
fnOptions: ?ComponentOptions; // pour le cache SSR
devtoolsMeta: ?Object; // utilisé pour stocker le contexte de rendu fonctionnel pour les outils de dev
fnScopeId: ?string; // support d'ID de scope fonctionnel
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
Ne vous laissez pas intimider par le grand nombre d'attributs de VNode ou n'essayez pas de comprendre l'objectif de chaque attribut à tout prix. En réalité, il suffit de comprendre quelques attributs clés, par exemple :
- L'attribut
tagcorrespond à l'attribut de balise de cevnode - L'attribut
datacontient, après rendu en nœud DOM réel, les classes, attributs, styles et événements liés au nœud - L'attribut
childrenest le nœud enfant duvnode - L'attribut
textest l'attribut de texte - L'attribut
elmest le nœud DOM réel correspondant à cevnode - L'attribut
keyest le marquage duvnode, qui peut améliorer l'efficacité du processusdiff
3.1.2. Processus de création de VNode dans le code source
(1) Initialisation de Vue
Lorsque nous instancions une instance Vue, c'est-à-dire new Vue(), nous exécutons en réalité la fonction définie dans src/core/instance/index.js.
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue est un constructeur et devrait être appelé avec le mot-clé `new`')
}
this._init(options)
}
En consultant la fonction Vue, nous savons que Vue ne peut être initialisé qu'avec le mot-clé new, puis en appelant la méthode this._init, définie dans src/core/instance/init.js.
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// Une série d'autres codes d'initialisation sont omis
if (vm.$options.el) {
console.log('vm.$options.el:',vm.$options.el);
vm.$mount(vm.$options.el)
}
}
(2) Montage de l'instance Vue
Dans Vue, le montage du DOM est effectué par la méthode d'instance $mount. Nous allons maintenant analyser l'implémentation du montage de la version compiler, dont le code source est défini dans le fichier src/platforms/web/entry-runtime-with-compiler.js.
const monter = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydratation?: boolean
): Component {
el = el && query(el)
// Une série d'autres codes d'initialisation et de jugement logique sont omis
return monter.call(this, el, hydratation)
}
Nous constatons qu'en fin de compte, la méthode $mount du prototype original est appelée pour le montage. La méthode $mount du prototype original est définie dans src/platforms/web/runtime/index.js.
Vue.prototype.$mount = function (
el?: string | Element,
hydratation?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return monterComposant(this, el, hydratation)
}
Nous constatons que la méthode $mount appelle en réalité la méthode monterComposant, définie dans le fichier src/core/instance/lifecycle.js.
export function monterComposant (
vm: Component,
el: ?Element,
hydratation?: boolean
): Component {
vm.$el = el
// Une série d'autres codes sont omis
let mettreAJourComposant
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mettreAJourComposant = () => {
// Génère un vnode virtuel
const vnode = vm._rendre()
// Met à jour le DOM
vm._mettreAJour(vnode, hydratation)
}
} else {
mettreAJourComposant = () => {
vm._mettreAJour(vm._rendre(), hydratation)
}
}
// Instancie un Watcher de rendu, dans sa fonction de rappel, appelle la méthode mettreAJourComposant
new Watcher(vm, mettreAJourComposant, noop, {
avant () {
if (vm._estMonte && !vm._estDetruit) {
appelerCrochet(vm, 'beforeUpdate')
}
}
}, true /* estWatcherDeRendu */)
hydratation = false
return vm
}
À partir du code ci-dessus, nous voyons que monterComposant instancie d'abord un Watcher de rendu. Dans sa fonction de rappel, il appelle la méthode mettreAJourComposant. Dans cette méthode, la méthode vm._rendre est appelée pour générer d'abord un Node virtuel, puis vm._mettreAJour est appelé pour mettre à jour le DOM.
(3) Création du Node virtuel
La méthode _rendre de Vue est une méthode privée de l'instance, utilisée pour rendre l'instance en un Node virtuel. Elle est définie dans le fichier src/core/instance/render.js :
Vue.prototype._rendre = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
// Une série de codes sont omis
instanceRenduActuelle = vm
// Appelle la méthode createElement pour retourner le vnode
vnode = render.call(vm._proxyRendu, vm.$creerElement)
} catch (e) {
gererErreur(e, vm, `rendu`)
}
// définit le parent
vnode.parent = _parentVnode
console.log("vnode...:",vnode);
return vnode
}
Vue.js utilise la méthode _creerElement pour créer un VNode, définie dans src/core/vdom/creer-element.js :
export function _creerElement (
contexte: Component,
tag?: string | Class<Component> | Function | Object,
donnees?: VNodeData,
enfants?: any,
normalisationType?: number
): VNode | Array<VNode> {
// Une série de codes non essentiels sont omis
if (normalisationType === TOUJOURS_NORMALISER) {
// Scène où la fonction de rendu n'est pas générée par compilation
enfants = normaliserEnfants(enfants)
} else if (normalisationType === SIMPLE_NORMALISER) {
// Scène où la fonction de rendu est générée par compilation
enfants = simpleNormaliserEnfants(enfants)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (contexte.$vnode && contexte.$vnode.ns) || config.getTagNamespace(tag)
if (config.estTagReserve(tag)) {
// Crée un vnode virtuel
vnode = new VNode(
config.parsePlatformTagName(tag), donnees, enfants,
undefined, undefined, contexte
)
} else if ((!donnees || !donnees.pre) && estDef(Ctor = resoudreRessource(contexte.$options, 'composants', tag))) {
// composant
vnode = creerComposant(Ctor, donnees, contexte, enfants, tag)
} else {
vnode = new VNode(
tag, donnees, enfants,
undefined, undefined, contexte
)
}
} else {
vnode = creerComposant(tag, donnees, contexte, enfants)
}
if (Array.isArray(vnode)) {
return vnode
} else if (estDef(vnode)) {
if (estDef(ns)) appliquerNS(vnode, ns)
if (estDef(donnees)) enregistrerLiensProfonds(donnees)
return vnode
} else {
return creerVideVNode()
}
}
La méthode _creerElement a 5 paramètres : contexte représante l'environnement de contexte du VNode, il est de type Component ; tag représète la balise, qui peut être une chaîne de caractères ou un Component ; donnees représente les données du VNode, c'est un type VNodeData, dont on peut trouver la définition dans flow/vnode.js ; enfants représente les nœuds enfants du VNode actuel, il peut être de n'importe quel type et doit être normalisé en un tableau standard de VNode ;
3.1.3. Exemple d'instance
Pour mieux comprendre comment notre code Vue habituel est représenté par la classe VNode, nous examinons une conversion d'instance pour une compréhension plus approfondie.
Par exemple, instancions une instance Vue :
var app = new Vue({
el: '#app',
render: function (creerElement) {
return creerElement('div', {
attrs: {
id: 'app',
class: "classe_boite"
},
}, this.message)
},
data: {
message: 'Bonjour Vue !'
}
})
Nous affichons sa représentation VNode correspondante :
3.2. Processus diff
3.2.1. Logique d'appel de diff dans le code source de Vue.js
Le code source de Vue.js instancie un watcher, qui est ajouté aux dépendances des variables liées dans le modèle. Une fois que les données réactives du modèle changent, le tableau dep maintenu par ces données réactives appelle la méthode dep.notify(), ce qui complète le travail d'exécution de toutes les dépendances, y compris la mise à jour de la vue, c'est-à-dire l'appel de la méthode mettreAJourComposant. Le watcher et la méthode mettreAJourComposant sont définis dans le fichier src/core/instance/lifecycle.js.
export function monterComposant (
vm: Component,
el: ?Element,
hydratation?: boolean
): Component {
vm.$el = el
// Une série d'autres codes sont omis
let mettreAJourComposant
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mettreAJourComposant = () => {
// Génère un vnode virtuel
const vnode = vm._rendre()
// Met à jour le DOM
vm._mettreAJour(vnode, hydratation)
}
} else {
mettreAJourComposant = () => {
vm._mettreAJour(vm._rendre(), hydratation)
}
}
// Instancie un Watcher de rendu, dans sa fonction de rappel, appelle la méthode mettreAJourComposant
new Watcher(vm, mettreAJourComposant, noop, {
avant () {
if (vm._estMonte && !vm._estDetruit) {
appelerCrochet(vm, 'beforeUpdate')
}
}
}, true /* estWatcherDeRendu */)
hydratation = false
return vm
}
La mise à jour de la vue est en fait l'appel de la méthode vm._mettreAJour, qui reçoit comme premier paramètre le VNode généré. La méthode vm._mettreAJour appelée est définie dans src/core/instance/lifecycle.js.
Vue.prototype._mettreAJour = function (vnode: VNode, hydratation?: boolean) {
const vm: Component = this
const ancienEl = vm.$el
const ancienVnode = vm._vnode
const restaurerInstanceActive = definirInstanceActive(vm)
vm._vnode = vnode
if (!ancienVnode) {
// Le premier paramètre est un nœud réel, c'est l'initialisation
vm.$el = vm.__corriger__(vm.$el, vnode, hydratation, false /* removeOnly */)
} else {
// Si le prevVnode à comparer existe, on effectue le diff entre prevVnode et vnode
vm.$el = vm.__corriger__(ancienVnode, vnode)
}
restaurerInstanceActive()
// met à jour la référence __vue__
if (ancienEl) {
ancienEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// si le parent est un HOC, met aussi à jour son $el
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
Dans cette méthode, la partie la plus importante est la méthode vm.__corriger__, qui est le cœur de l'ensemble du virtual-dom. Elle termine principalement le processus diff entre prevVnode et vnode, applique les opérations nécessaires sur les nœuds vdom, et génère finalement de nouveaux nœuds DOM réels pour compléter la mise à jour de la vue.
Examinons maintenant le processus logique de vm.__corriger__. La méthode vm.__corriger__ est définie dans src/core/vdom/corriger.js.
function corriger (ancienVnode, vnode, hydratation, supprimerSeulement) {
......
if (estIndef(ancienVnode)) {
// Quand ancienVnode n'existe pas, crée un nouveau nœud
estPremierCorrectif = true
creerElm(vnode, insereVnodeFile)
} else {
// Effectue le diff entre ancienVnode et vnode, et applique un correctif à ancienVnode
const estElementReel = estDef(ancienVnode.nodeType)
if (!estElementReel && memeVnode(ancienVnode, vnode)) {
// corrige le nœud racine existant
corrigerVnode(ancienVnode, vnode, insereVnodeFile, null, null, supprimerSeulement)
}
......
}
}
Dans la méthode corriger, nous voyons qu'il y a deux cas : l'un est quand ancienVnode n'existe pas, un nouveau nœud est créé ; l'autre est quand ancienVnode existe déjà, alors un diff et un patch sont effectués entre ancienVnode et vnode. Dans le processus patch, la méthode memeVnode est appelée pour comparer les attributs de base des deux vnode entrants. Seulement quand les attributs de base sont identiques, on considère que ces deux vnode ont été mis à jour localement, et alors un diff est effectué sur ces deux vnode. Si les attributs de base des deux vnode sont différents, le processus diff est sauté, et un nouveau vnode est créé pour remplacer l'ancien vnode.
function memeVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
estDef(a.data) === estDef(b.data) &&
memeTypeEntree(a, b)
)
}
Le processus diff est principalement effectué en appelant la méthode corrigerVnode :
function corrigerVnode (ancienVnode, vnode, insereVnodeFile, tableauParent, index, supprimerSeulement) {
......
const elm = vnode.elm = ancienVnode.elm
const anciensEnfants = ancienVnode.children
const enfants = vnode.children
// Si vnode n'a pas de nœud texte
if (estIndef(vnode.text)) {
// Si l'attribut children de ancienVnode existe et celui de vnode aussi
if (estDef(anciensEnfants) && estDef(enfants)) {
// met à jour les enfants, effectue un diff sur les enfants
if (anciensEnfants !== enfants) mettreAJourEnfants(elm, anciensEnfants, enfants, insereVnodeFile, supprimerSeulement)
} else if (estDef(enfants)) {
if (process.env.NODE_ENV !== 'production') {
verifierDoublonsCles(enfants)
}
// Si ancienVnode a du texte, d'abord vide le contenu du texte, puis ajoute les enfants de vnode
if (estDef(ancienVnode.text)) operationsNoeud.setTextContent(elm, '')
ajouterVnoeuds(elm, null, enfants, 0, enfants.length - 1, insereVnodeFile)
} else if (estDef(anciensEnfants)) {
// Supprime les anciens enfants sous elm
supprimerVnoeuds(elm, anciensEnfants, 0, anciensEnfants.length - 1)
} else if (estDef(ancienVnode.text)) {
// ancienVnode a des enfants, mais vnode n'en a pas, donc vide ce nœud
operationsNoeud.setTextContent(elm, '')
}
} else if (ancienVnode.text !== vnode.text) {
// Si les attributs texte de ancienVnode et vnode sont différents, remplace directement l'élément texte du DOM réel
operationsNoeud.setTextContent(elm, vnode.text)
}
......
}
À partir du code ci-dessus, nous constatons que,
Le processus diff se divise en plusieurs cas, anciensEnfants sont les enfants de ancienVnode, enfants sont les enfants de Vnode :
- D'abord, on vérifie les nœuds texte. Si
ancienVnode.text !== vnode.text, alors le nœud texte est directement remplacé ; - Quand
vnoden'a pas de nœud texte, on entre dans lediffdes enfants ; - Quand
anciensEnfantsetenfantsexistent et sont différents, on appellemettreAJourEnfantspour effectuer undiffsur les enfants ; - Si
anciensEnfantsn'existe pas maisenfantsexiste, on vide d'abord le nœud texte deancienVnode, et on appelle la méthodeajouterVnoeudspour ajouterenfantsau nœud DOM réelelm; - Si
anciensEnfantsexiste maisenfantsn'existe pas, on supprime lesanciensEnfantssous le nœud réelelm; - Si
ancienVnodea un nœud texte maisvnoden'en a pas, on vide ce nœud texte.
3.2.2. Analyse du flux de diff des enfants
(1) Code source de Vue.js
Ici, nous analysons en détail la méthode mettreAJourEnfants, qui est l'étape la plus importante de l'ensemble du processus diff. Le processus du code source de Vue.js est le suivant. Pour mieux comprendre le processus diff, nous fournissons des diagrammes correspondants pour l'explication.
function mettreAJourEnfants (elmParent, anciensEnfants, nouveauxEnfants, insereVnodeFile, supprimerSeulement) {
// Établit des index pour anciensEnfants et nouveauxEnfants, respectivement, comme base pour la traversée ultérieure
let indexDebutAncien = 0
let indexDebutNouveau = 0
let indexFinAncien = anciensEnfants.length - 1
let debutAncienVnode = anciensEnfants[0]
let finAncienVnode = anciensEnfants[indexFinAncien]
let indexFinNouveau = nouveauxEnfants.length - 1
let debutNouveauVnode = nouveauxEnfants[0]
let finNouveauVnode = nouveauxEnfants[indexFinNouveau]
let ancienCleVersIndex, indexDansAncien, vnodeADeplacer, refElm
// Continue la boucle jusqu'à ce que anciensEnfants ou nouveauxEnfants soient entièrement parcourus
while (indexDebutAncien <= indexFinAncien && indexDebutNouveau <= indexFinNouveau) {
if (estIndef(debutAncienVnode)) {
debutAncienVnode = anciensEnfants[++indexDebutAncien] // Le Vnode a été déplacé vers la gauche
} else if (estIndef(finAncienVnode)) {
finAncienVnode = anciensEnfants[--indexFinAncien]
} else if (memeVnode(debutAncienVnode, debutNouveauVnode)) {
corrigerVnode(debutAncienVnode, debutNouveauVnode, insereVnodeFile, nouveauxEnfants, indexDebutNouveau)
debutAncienVnode = anciensEnfants[++indexDebutAncien]
debutNouveauVnode = nouveauxEnfants[++indexDebutNouveau]
} else if (memeVnode(finAncienVnode, finNouveauVnode)) {
corrigerVnode(finAncienVnode, finNouveauVnode, insereVnodeFile, nouveauxEnfants, indexFinNouveau)
finAncienVnode = anciensEnfants[--indexFinAncien]
finNouveauVnode = nouveauxEnfants[--indexFinNouveau]
} else if (memeVnode(debutAncienVnode, finNouveauVnode)) { // Vnode déplacé vers la droite
corrigerVnode(debutAncienVnode, finNouveauVnode, insereVnodeFile, nouveauxEnfants, indexFinNouveau)
peutDeplacer && operationsNoeud.insererAvant(elmParent, debutAncienVnode.elm, operationsNoeud.suivant(finAncienVnode.elm))
debutAncienVnode = anciensEnfants[++indexDebutAncien]
finNouveauVnode = nouveauxEnfants[--indexFinNouveau]
} else if (memeVnode(finAncienVnode, debutNouveauVnode)) { // Vnode déplacé vers la gauche
corrigerVnode(finAncienVnode, debutNouveauVnode, insereVnodeFile, nouveauxEnfants, indexDebutNouveau)
peutDeplacer && operationsNoeud.insererAvant(elmParent, finAncienVnode.elm, debutAncienVnode.elm)
finAncienVnode = anciensEnfants[--indexFinAncien]
debutNouveauVnode = nouveauxEnfants[++indexDebutNouveau]
} else {
if (estIndef(ancienCleVersIndex)) ancienCleVersIndex = creerCleVersAncienIndex(anciensEnfants, indexDebutAncien, indexFinAncien)
indexDansAncien = estDef(debutNouveauVnode.key)
? ancienCleVersIndex[debutNouveauVnode.key]
: trouverIndexDansAncien(debutNouveauVnode, anciensEnfants, indexDebutAncien, indexFinAncien)
if (estIndef(indexDansAncien)) { // Nouvel élément
creerElm(debutNouveauVnode, insereVnodeFile, elmParent, debutAncienVnode.elm, false, nouveauxEnfants, indexDebutNouveau)
} else {
vnodeADeplacer = anciensEnfants[indexDansAncien]
if (memeVnode(vnodeADeplacer, debutNouveauVnode)) {
corrigerVnode(vnodeADeplacer, debutNouveauVnode, insereVnodeFile, nouveauxEnfants, indexDebutNouveau)
anciensEnfants[indexDansAncien] = undefined
peutDeplacer && operationsNoeud.insererAvant(elmParent, vnodeADeplacer.elm, debutAncienVnode.elm)
} else {
// même clé mais élément différent. Traite comme nouvel élément
creerElm(debutNouveauVnode, insereVnodeFile, elmParent, debutAncienVnode.elm, false, nouveauxEnfants, indexDebutNouveau)
}
}
debutNouveauVnode = nouveauxEnfants[++indexDebutNouveau]
}
}
if (indexDebutAncien > indexFinAncien) {
refElm = estIndef(nouveauxEnfants[indexFinNouveau + 1]) ? null : nouveauxEnfants[indexFinNouveau + 1].elm
ajouterVnoeuds(elmParent, refElm, nouveauxEnfants, indexDebutNouveau, indexFinNouveau, insereVnodeFile)
} else if (indexDebutNouveau > indexFinNouveau) {
supprimerVnoeuds(elmParent, anciensEnfants, indexDebutAncien, indexFinAncien)
}
}
Avant de commencer le parcours diff, on d'abord assigne un startIndex et un endIndex à anciensEnfants et nouveauxEnfants respectivement comme base pour le parcours. La condition d'arrêt du parcours diff est quand anciensEnfants ou nouveauxEnfants sont entièrement parcourus (la condition est startIndex >= endIndex). Ensuite, regardons le processus diff entier à travers un exemple (cas où les nœuds n'ont pas de key).
(2) Processus diff sans key
Nous expliquons le processus de code ci-dessus à travers le schéma suivant :
(2.1) D'abord, on commence à comparer à partir du premier nœud. Que ce soit pour anciensEnfants ou nouveauxEnfants, les nœuds de début ou de fin ne satisfont pas memeVnode, et les attributs des nœuds n'ont pas de marquage key. Ainsi, après le premier tour de diff, le debutNouveauVnode de nouveauxEnfants est ajouté avant debutAncienVnode, et indexDebutNouveau est déplacé d'une position vers la gauche ;
2.2) Dans le deuxième tour de diff, la condition memeVnode(debutAncienVnode, debutNouveauVnode) est satisfaite. Ainsi, un diff est effectué sur ces deux vnode, et le correctif est appliqué à debutAncienVnode. En même temps, debutAncienVnode et indexDebutNouveau se déplacent tous les deux d'une position vers l'avant (2.3) Dans le troisième tour de diff, la condition memeVnode(finAncienVnode, debutNouveauVnode) est satisfaite. D'abord, un diff est effectué sur finAncienVnode et debutNouveauVnode, et un correctif est appliqué à finAncienVnode. L'opération de déplacement de finAncienVnode est complétée. Enfin, indexDebutNouveau se déplace d'une position vers la gauche, et debutAncienVnode d'une position vers la droite ;
(2.4) Dans le quatrième tour de diff, le processus est similaire à l'étape 3 ;
2.5) Dans le cinquième tour de diff, similaire au processus 1 ;
(2.6) Après la fin du parcours, indexDebutNouveau > indexFinNouveau, ce qui indique que anciensEnfants a des nœuds supplémentaires. Finalement, ces nœuds supplémentaires doivent être supprimés.
3) Processus diff avec key
Quand les vnode ont un attribut key, dans chaque tour du processus diff, quand les nœuds de début et de fin ne trouvent pas de memeVnode, on vérifie ensuite si debutNouveauVnode a un attribut key et si on trouve un nœud correspondant dans ancienCleVersIndex :
- Si cete
keyn'existe pas, alorsdebutNouveauVnodeest créé comme un nouveau nœud et inséré dans les enfants de la racine existante ; - Si cette
keyexiste, alors on prend levnodedansanciensEnfantsqui a cettekey, et on effectue le processusdiff;
À travers cette analyse, après avoir ajouté l'attribut key au vdom, dans le parcours diff, quand les recherches et le diff aux points de début et de fin ne peuvent toujours pas matcher, on utilise la key comme identifiant unique pour effectuer le diff, ce qui peut améliorer l'efficacité du diff.
Le processus diff de vnode avec l'attribut Key est illustré dans la figure suivante :
(3.1) D'abord, on commence à comparer à partir du premier nœud. Que ce soit pour anciensEnfants ou nouveauxEnfants, les nœuds de début ou de fin ne satisfont pas memeVnode, mais les attributs des nœuds ont un marquage key. Ensuite, on trouve le nœud correspondant dans ancienCleVersIndex. Ainsi, après le premier tour de diff, le nœud B dans anciensEnfants est supprimé, mais le nœud B dans nouveauxEnfants garde la référence elm du nœud B dans anciensEnfants.
3.2) Dans le deuxième tour de diff, la condition memeVnode(debutAncienVnode, debutNouveauVnode) est satisfaite. Ainsi, un diff est effectué sur ces deux vnode, et le correctif est appliqué à debutAncienVnode. En même temps, debutAncienVnode et indexDebutNouveau se déplacent tous les deux d'une position vers l'avant 3.3) Dans le troisième tour de diff, la condition memeVnode(finAncienVnode, debutNouveauVnode) est satisfaite. D'abord, un diff est effectué sur finAncienVnode et debutNouveauVnode, et un correctif est appliqué à finAncienVnode. L'opération de déplacement de finAncienVnode est complétée. Enfin, indexDebutNouveau se déplace d'une position vers la gauche, et debutAncienVnode d'une position vers la droite ; (3.4) Dans le quatrième tour de diff, le processus est similaire à l'étape 2 ;
(3.5) Dans le cinquième tour de diff, comme indexDebutAncien est déjà supérieur à indexFinAncien, on insère la file restante de Vnode à la fin de la file.
3.3. Processus patch
À travers l'analyse du processus diff dans la section 3.2, nous voyons que les méthodes operationsNoeud effectuent des opérations sur la structure DOM réelle. operationsNoeud est défini dans src/platforms/web/runtime/operations-noeud.js, et il s'agit d'opérations DOM de base. Nous ne détaillerons pas cela ici.
export function creerElementNS (espaceNoms: string, nomBalise: string): Element {
return document.createElementNS(espaceNomsMap[espaceNoms], nomBalise)
}
export function creerTexte (texte: string): Text {
return document.createTextNode(texte)
}
export function creerCommentaire (texte: string): Comment {
return document.createComment(texte)
}
export function insererAvant (noeudParent: Node, nouveauNoeud: Node, noeudReference: Node) {
noeudParent.insertBefore(nouveauNoeud, noeudReference)
}
export function supprimerEnfant (noeud: Node, enfant: Node) {
noeud.removeChild(enfant)
}
3.4. Conclusion
À travers les trois premières sous-sections, nous avons analysé le processus principal de la façon dont le modèle et les données sont rendus en DOM final. Nous pouvons voir de manière plus intuitive le processus d'initialisation de Vue au rendu final à travers le schéma ci-dessous.
Quatre. Conclusion
Cet article a d'abord présenté le processus de parsing du vrai DOM et ses problèmes, justifiant ainsi la nécessité du DOM virtuel. Ensuite, il a analysé les avantages du DOM virtuel, ainsi que certaines bases théoriques et l'implémantation d'algorithmes de base. Enfin, en nous basant sur les connaissances de base acquises, nous avons examiné étape par étape comment le code source de Vue.js les met en œuvre. Du problème existant → bases théoriques → pratique spécifique, nous avons progressé en profondeur pour aider à mieux comprendre ce qu'est le DOM virtuel, pourquoi il est nécessaire, et comment il est implémenté. Nous espérons que cet article vous sera utile.
Auteur :随风而逝_风逝 Lien : https://juejin.cn/post/6844903895467032589Source : Juejin Droits d'auteur de l'auteur. Pour la redistribution commerciale, veuillez contacter l'auteur pour obtenir l'autorisation. Pour la redistribution non commerciale, veuillez indiquer la source.