Dans Vue.js, la méthode nextTick est essentielle pour exécuter du code de manière asynchrone après la prochaine mise à jour du DOM. Son fonctionnement repose sur le mécanisme de la boucle d'événements en JavaScript. Nous allons examiner son code source et explorer les concepts sous-jacents.
Emplacement du code source
L'implémentation de nextTick se trouve dans le fichier src/core/util/next-tick.js du dépôt Vue.js.
Blocs de code principaux
L'approche asynchrone est choisie en fonction de l'environnement d'exécution. Voici une version restructurée du code qui sélectionne la méthode appropriée :
// Déterminer l'exécuteur asynchrone basé sur les capacités du navigateur
let asyncExecutor;
let usesMicrotask = false;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const promiseInstance = Promise.resolve();
asyncExecutor = () => {
promiseInstance.then(processPendingCallbacks);
if (isIOS) setTimeout(noop);
};
usesMicrotask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')) {
let iterationCounter = 1;
const mutationObserver = new MutationObserver(processPendingCallbacks);
const textNode = document.createTextNode(String(iterationCounter));
mutationObserver.observe(textNode, {
characterData: true
});
asyncExecutor = () => {
iterationCounter = (iterationCounter + 1) % 2;
textNode.data = String(iterationCounter);
};
usesMicrotask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
asyncExecutor = () => {
setImmediate(processPendingCallbacks);
};
} else {
asyncExecutor = () => {
setTimeout(processPendingCallbacks, 0);
};
}
L'implémentation de la fonction nextTick elle-même :
export function nextTick(callbackFunction, context) {
let promiseResolve;
callbackQueue.push(() => {
if (callbackFunction) {
try {
callbackFunction.call(context);
} catch (error) {
handleError(error, context, 'nextTick');
}
} else if (promiseResolve) {
promiseResolve(context);
}
});
if (!isProcessing) {
isProcessing = true;
asyncExecutor();
}
if (!callbackFunction && typeof Promise !== 'undefined') {
return new Promise(resolve => {
promiseResolve = resolve;
});
}
}
La fonction processPendingCallbacks qui vide la file d'attente :
function processPendingCallbacks() {
isProcessing = false;
const pendingCallbacks = callbackQueue.slice(0);
callbackQueue.length = 0;
for (let index = 0; index < pendingCallbacks.length; index++) {
pendingCallbacks[index]();
}
}
Principe de fonctionnement
Le mécanisme de nextTick exploite la boucle d'événements de JavaScript pour ajouter des tâches micro ou macro, garantissant que les callbacks s'exécutent de manière asynchrone, typiquement après une mise à jour des données dans Vue.
La boucle d'événements en JavaScript
JavaScript s'exécute sur un seul thread principal qui traite les tâches synchrones, formant une pile d'exécution. En parallèle, il existe une file d'attente pour les tâches asynchrones. Lorsque la pile d'exécution est vide, le moteur JavaScript récupère les callbacks de la file d'attente et les ajoute au thread principal. Ce processsu se répète dans chaque cycle d'événement.
Tâches macro et micro courantes
- Tâches macro :
- Opérations d'E/S
setTimeout– Planifie l'exécution après un délai minimum, souvant 1 ms par défaut dans les navigateurssetInterval– Similaire àsetTimeoutmais répétitifsetImmediate– Exécute après les E/S en cours, peut être plus rapide quesetTimeout(fn, 0)dans certains casrequestAnimationFrame– Synchronisé avec le rafraîchissement de l'écran du navigateur
- Tâches micro :
process.nextTick– Exécuté à la fin du cycle actuel, avant les autres tâches micro ou macroMutationObserver– Déclenché lors de modifications du DOM, ajouté à la file des microtâchesPromise.then(aveccatchetfinally) – Intégré directement dans la file des microtâches
Ordre d'exécution
Dans chaque cycle d'événement, toutes les microtâches sont exécutées avant les macrotâches, y compris celles ajoutées dynamiquement pendant l'exécution des microtâches. Voici des exemples illustrant ce comportement :
// Exemple 1 : Avec seulement des macrotâches, l'ordre peut varier (123, 132, 213, etc.)
setTimeout(() => {
console.log('A');
}, 0);
setImmediate(() => {
console.log('B');
});
setTimeout(() => {
console.log('C');
}, 0);
// Exemple 2 : Lorsque le thread principal a d'autres tâches, setImmediate tend à s'exécuter après setTimeout(fn, 0)
setTimeout(() => {
console.log('1'); // Première macrotâche
}, 0);
setImmediate(() => {
console.log('2'); // Troisième macrotâche
});
setTimeout(() => {
console.log('3'); // Deuxième macrotâche
}, 0);
new Promise((resolve) => {
console.log('4'); // Exécution synchrone dans le thread principal
resolve();
}).then(() => {
console.log('5'); // Deuxième microtâche
}).finally(() => {
console.log('6'); // Troisième microtâche
});
process.nextTick(() => {
console.log('7'); // Première microtâche
});
console.log('8'); // Exécution synchrone dans le thread principal
Dans ce cas, les résultats typiques sont 4, 8, 7, 5, 6, 1, 3, 2, mais cela peut varier selon les environnements.
// Exemple 3 : Les microtâches dynamiques sont exécutées dans le même cycle
process.nextTick(() => {
console.log('i'); // Première microtâche
setTimeout(() => {
console.log('ii'); // Macrotâche du prochain cycle
}, 0);
Promise.resolve('iii').then((data) => {
console.log(data); // Microtâche du même cycle
});
});
Promise.resolve('iv').then(data => {
console.log(data); // Deuxième microtâche
});
setTimeout(() => {
console.log('v'); // Première macrotâche du prochain cycle
Promise.resolve('vi').then(data => {
console.log(data); // Microtâche du prochain cycle
});
process.nextTick(() => {
console.log('vii'); // Microtâche du prochain cycle
});
setTimeout(() => {
console.log('viii'); // Deuxième macrotâche du prochain cycle
});
});
Ici, l'ordre d'exécution est généralement : i, iv, iii, v, vii, vi, ii, viii, car les microtâches doivent toutes être traitées avant de passer aux macrotâches dans chaque cycle.