Le DOM Virtuel
Les manipulations directes du DOM sont coûteuses en ressources. Chaque modification peut déclencher des recalculs de mise en page (reflow) et des rafraîchissements visuels (repaint), forçant le navigateur à redessiner l'interface. Les frameworks modernes comme Vue et React adoptent une approche pilotée par les données, minimisant ainsi les interactions directes avec le DOM réel.
Pour orchestrer ces mises à jour de manière optimale, ils s'appuient sur le DOM virtuel (VDOM). Cette technique consiste à représenter l'arborescence du DOM sous forme d'objets JavaScript légers. Les calculs de différence sont effectués en mémoire via un algorithme de diff, puis seules les modifications minimales nécessaires sont appliquées au DOM réel.
L'implémentation de Vue s'inspire fortement de la bibliothèque Snabbdom. Voici une illustration de son fonctionnement :
import { init, classModule, propsModule, styleModule, eventListenersModule, h } from "snabbdom";
const applyPatch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
const rootElement = document.getElementById("app-root");
const initialTree = h("section#app-root.main-wrapper", { on: { click: handleInteraction } }, [
h("strong", { style: { color: "blue" } }, "Texte mis en évidence"),
" et un texte standard",
h("a", { props: { href: "/dashboard" } }, "Accéder au tableau de bord"),
]);
applyPatch(rootElement, initialTree);
const updatedTree = h("section#app-root.main-wrapper", { on: { click: handleNewInteraction } }, [
h("em", { style: { color: "red", fontStyle: "italic" } }, "Texte modifié"),
" et un texte standard inchangé",
h("a", { props: { href: "/settings" } }, "Accéder aux paramètres"),
]);
applyPatch(initialTree, updatedTree);
Deux fonctions sont au cœur de ce mécanisme :
- La fonction
h(hyperscript) : Elle génère un nœud virtuel (vnode) en prenant un sélecteur, un objet de données (attributs, styles, événements) et des enfants. - La fonction
patch: Elle assure deux rôles. D'abord, le montage initial en convertissant le vnode en DOM réel. Ensuite, la comparaison entre l'ancien et le nouveau vnode pour appliquer les mises à jour différentielles.
L'Algorithme de Diff
La comparaison de deux arbres DOM complets aurait une complexité algorithmique de O(n³), ce qui est inenvisageable pour des interfaces complexes. Les frameworks appliquent donc des heuristiques strictes pour ramener cette complexité à O(n) :
- La comparaison s'effectue uniquement entre nœuds de même niveau (pas de comparaison inter-niveaux).
- Si les balises (tags) diffèrent, le nœud est détruit et recréé, sans analyse de ses enfants.
- Si la balise et la clé (
key) sont identiques, le nœud est considéré comme le même et l'algorithme poursuit la comparaison en profondeur.
Mécanisme de mise à jour des enfants
Lors de la mise à jour des listes d'enfants via la fonction updateChildren, l'algorithme utilise quatre pointeurs : oldStart, oldEnd, newStart, et newEnd. Ces pointeurs convergent vers le centre de la liste. À chaque itération, il compare les extrémités (début avec début, fin avec fin, début avec fin, etc.). Si une correspondance est trouvée, il déplace les pointeurs et applique un patchVnode récursif.
Si aucune correspondance d'extrémité n'est trouvée, il recherche la clé du nouveau nœud dans l'ancienne liste. Si la clé existe et que le sélecteur correspond, le nœud est déplacé et mis à jour. Sinon, un nouveau nœud est inséré.
L'importance de l'attribut key
Dans les directives de boucle comme v-for, l'attribut key est crucial. Sans clé unique, l'algorithme se base sur l'index, ce qui peut entraîner des destructions et recréations inutiles lors de l'insertion ou du tri d'éléments, en plus de causer des erreurs d'état local. Une clé unique et stable permet au moteur de diff de réordonner les éléments existants de manière optimale.
Compilation des Templates
Les templates Vue ne sont pas du HTML standard ; ils contiennent des directives et des expressions. Étant donné que le HTML n'est pas Turing-complet, le template doit être transformé en code JavaScript exécutable. C'est le rôle du compilateur de templates.
Le compilateur transforme le template en une fonction render. Historiquement, cette fonction utilise l'instruction with de JavaScript pour résoudre les variables dans le contexte de l'instance Vue.
const compiler = require('vue-template-compiler');
const template = `<article><h1>{{ userGreeting }}</h1></article>`;
const compiled = compiler.compile(template);
console.log(compiled.render);
Cela génère une fonction utilisant des helpers internes comme _c (createElement), _v (createTextVNode), et _s (toString) :
with(this){return _c('article',[_c('h1',[_v(_s(userGreeting))])])}
Traduction des dircetives
Les différentes fonctionnalités du template sont mappées vers des appels de fonctions spécifiques :
- Conditions (
v-if) : Converties en expressions ternaires JavaScript. - Boucles (
v-for) : Utilisent la fonction_l(renderList) pour itérer et générer un tableau de vnodes. - Événements (
@click) : Injectés dans l'objet de données du vnode sous la propriétéon. - Liaison bidirectionnelle (
v-model) : Compilée en une combinaison d'attributvalueet d'écouteur d'événementinput.
// Exemple de compilation d'une boucle
const loopTemplate = `
<nav>
<a v-for="link in navigationLinks" :key="link.id" :href="link.url">{{ link.label }}</a>
</nav>
`;
// Résultat simplifié :
// with(this){return _c('nav',_l((navigationLinks),function(link){return _c('a',{key:link.id,attrs:{"href":link.url}},[_v(_s(link.label))])}),0)}
En environnement de production, via des outils comme vue-loader, cette compilation est effectuée en amont (build time), éliminant le besoin d'inclure le compilateur dans le bundle final.
Cycle de Rendu et Mises à Jour
Le cycle de vie du rendu se décompose en deux phases distinctes :
- Rendu initial : Le template est compilé en fonction
render. Le système de réactivité est initialisé (interception des getters/setters). La fonctionrenderest exécutée pour produire le VDOM initial, qui est ensuite monté viapatch. - Mise à jour : Une mutation de donnée déclenche un setter. Le composateur planifie un nouveau rendu. La fonction
rendergénère un nouveau VDOM, et l'algorithme de diff calcule et applique les modifications minimales au DOM réel.
Rendu Asynchrone et File d'Attente
Vue n'applique pas les mises à jour du DOM de manière synchrone. Lorsqu'une donnée change, le composant est placé dans une file d'attente. Si la même donnée est modifiée plusieurs fois dans le même cycle d'événement (event loop), le composant n'est rendu qu'une seule fois avec l'état final. La méthode this.$nextTick() permet d'exécuter un callback uniquement après que la file d'attente de mise à jour du DOM a été vidée et le DOM réel mis à jour.
Architecture MVVM
Contrairement au modèle MVC où le Contrôleur gère explicitement les mises à jour du DOM, le modèle MVVM (Model-View-ViewModel) introduit une liaison de données bidirectionnelle. Le ViewModel agit comme un convertisseur abstrait : il expose les données du Model à la View et synchronise automatiquement les modifications de la View vers le Model. Cette approche élimine les manipulations manuelles du DOM, améliorant ainsi la maintenabilité et les performances.
Systèmes de Réactivité
Le moteur de réactivité est le fondement de la synchronisation entre les données et la vue :
- Vue 2 : Utilise
Object.defineProperty()pour intercepter les accès et mutations des propriétés. Cette approche nécessite une traversée récursive de l'objet à l'initialisation et ne peut pas détecter nativement l'ajout/suppression de propriétés ou les mutations d'index de tableau. - Vue 3 : Repose sur l'API
Proxyde JavaScript. Cela permet d'intercepter toutes les opérations sur l'objet (y compris l'ajout de nouvelles propriétés et les opérations sur les tableaux) de manière plus performante et sans les limitations dedefineProperty.