Este artículo se centra en los principios relacionados con las actualizaciones asíncronas en Vue. La comprensión central de este artículo no es tan difícil como los principios de la reactividad, se centra en una palabra clave: cola.
Actualizaciones asíncronas
¿Por qué necesitamos actualizaciones asíncronas?
this.a = 1;this.b = 2;this.c = 3;this.a = 5;this.a = 6;Piénsalo, según el principio de reactividad de Vue que se analizó en el artículo anterior, cuando se asignan valores a datos reactivos, se ejecuta el setter y se actualiza la vista. Pero como se muestra en el ejemplo anterior, es muy común tener escenarios de asignación continua. En React, puedes escribirlo dentro de un setState, pero Vue no tiene ese método.
No se puede actualizar la página inmediatamente cada vez que se actualizan los datos. Si cada asignación ejecuta render y patch, ¿no se volvería la página extremadamente lenta?
La clave para resolver este problema es almacenar las funciones de actualización que deben ejecutarse en una cola (y al agregarlas a la cola, las funciones duplicadas no se agregarán nuevamente) y luego ejecutar la cola de forma asíncrona.
Variables relacionadas
Antes de analizar en detalle el principio de la cola asíncrona de Vue, echemos un vistazo a estas variables utilizadas para implementar la cola:
var circular = {}; // Detección de referencia circular en el entorno de desarrollovar queue = []; // Cola de watchersvar activatedChildren = [];var has = {}; // Indica si ya se ha agregado a la colavar waiting = false; // Se puede entender como si se está esperando el proceso de flushingvar flushing = false; // Indica si ya se ha comenzado a procesar la colavar index = 0; // Indica en qué posición de la cola se encuentra actualmenteEs posible que te preguntes por qué hay dos variables similares, waiting y flushing.
Al principio, también me confundía. ¿Está ocurriendo el proceso de flushing? ¿Está esperando el proceso de flushing (waiting)? Aunque hay una ligera diferencia:
- Durante
waiting, la tarea asíncrona no ha comenzado. - Durante
flushing, se ha confirmado que se ha iniciado la tarea de la cola.
Hay una pequeña diferencia en el momento de inicio, pero el momento de reinicio (resetSchedulerState) es el mismo para ambas variables. Parece que no hay necesidad de tener dos variables, así que por ahora lo entenderé como una forma de hacer que el código sea más autoexplicativo.
Volviendo a la cola, este sistema tiene dos colas. Como se mencionó en el artículo anterior, queueWatcher coloca el watcher que necesita actualizarse en la cola para actualizarlo en la siguiente ocasión. Ahora, analicemos en detalle qué operaciones realiza queueWatcher.
queueWatcher
function queueWatcher(watcher) { var id = watcher.id; if (has[id] == null) { has[id] = true; // Marca el watcher como agregado a la cola if (!flushing) { // Si la cola no ha comenzado a actualizarse, simplemente se agrega a la cola queue.push(watcher); } else { // Si la cola está en proceso, Vue simplemente inserta el nuevo watcher en la posición más reciente en la que se ejecutó // Esto implica una lógica oculta // Los watchers que ya se han ejecutado se ejecutarán nuevamente de inmediato en el siguiente var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // Encolar el proceso de flushing if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } }}La última línea nextTick(flushSchedulerQueue) es para poner la cola de observadores en la cola de nextTick (aunque no se pasa ningún argumento, flushSchedulerQueue puede leer la cola que queueWatcher maneja).
Esta breve llamada de función conecta dos puntos clave: flushSchedulerQueue es el núcleo para manejar la cola de observadores, mientras que nextTick es el núcleo para el renderizado asíncrono de Vue.
flushSchedulerQueue
Según el comentario oficial, queue.sort ordena la cola para garantizar:
- Los componentes padres se ejecutan antes que los componentes hijos.
- Las funciones de observadores definidas por el usuario se ejecutan antes que los observadores de renderizado.
- Si un componente se destruye antes que su componente padre, se puede omitir este observador (a través de la propiedad activa del observador).
function flushSchedulerQueue() { flushing = true; var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
// La longitud de la cola puede cambiar, por lo que no se puede almacenar en caché queue.length for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; // Elimina el observador ya procesado de has watcher.run(); }
// Mantén copias de las colas posteriores antes de restablecer el estado var activatedQueue = activatedChildren.slice(); var updatedQueue = queue.slice();
resetSchedulerState(); // Restablece las variables relacionadas con la cola
callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue);}En resumen, flushSchedulerQueue se utiliza para manejar la cola que queueWatcher ha organizado.
Aquí hay una pregunta práctica simple: ¿Se puede acceder al último DOM en la función de devolución de llamada de un observador que observa un valor?
La respuesta se encuentra en el código anterior: durante el bucle de procesamiento de queue en la función flushSchedulerQueue, queue sigue aceptando observadores que queueWatcher ha insertado. Esto se utiliza principalmente para las funciones de observadores personalizadas o las actualizaciones desencadenadas entre componentes, que se insertan directamente en la cola en orden de actualización, sin esperar al siguiente bucle.
Por lo tanto, no asuma que al observar un cambio en un valor, puede obtener el último DOM en la función de devolución de llamada, porque se ejecutan en el mismo tick (y la función de renderizado del componente siempre se ejecuta al final debido a la clasificación).
nextTick
var nextTick = (function() { var callbacks = [] var pending = false var timerFunc
function nextTickHandler() { pending = false var copies = callbacks.slice(0) callbacks.length = 0 for (var i = 0; i < copies.length; i++) { copies[i]() } }if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()Let me explain how Vue creates asynchronous execution:
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()Déjame explicarte cómo Vue crea la ejecución asíncrona:
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()Un mecanismo de aplazamiento asíncrono. Antes de la versión 2.4, solíamos usar microtareas (Promise/MutationObserver), pero las microtareas tienen una prioridad demasiado alta y se ejecutan entre eventos secuenciales (por ejemplo, #4521, #6690) o incluso entre la propagación del mismo evento (#6566). Técnicamente, setImmediate debería ser la elección ideal, pero no está disponible en todas partes; y el único polyfill que encola consistentemente la devolución de llamada después de todos los eventos DOM desencadenados en el mismo bucle es mediante el uso de MessageChannel.
Según los comentarios oficiales, inicialmente Vue utilizaba microtareas como Promise para actualizar la cola, pero se encontró que el momento de actualización no era el adecuado, se insertaba la actualización en momentos incorrectos. Luego se realizó una modificación.
Aquí, Vue proporciona 4 formas de crear llamadas asíncronas:
setImmediateMessageChannel, se puede utilizar en IE 10 o superior, es muy sorprendente, no lo sabrías si no lo buscas, el alcance deMessageChanneles incluso mayor que el dePromise.Promise, según los comentarios, se utiliza en Weex.setTimeout 0, se utiliza en IE 10 o inferior.
¿En resumen, qué es nextTick?
Desde el código, se puede ver que es una función de ejecución inmediata que devuelve una función queueNextTick. Por lo tanto, en realidad estamos utilizando queueNextTick(cb, ctx) (aunque en realidad no entiendo por qué se envuelven las variables callbacks, timerFunc, etc. en un cierre en lugar de colocarlas directamente afuera).
En el caso de nextTick(flushSchedulerQueue), flushSchedulerQueue es la función de devolución de llamada cb.
- En primer lugar,
cbse inserta encallbacks, que es una cola que se ejecuta de forma asíncrona y no tiene nada que ver con la cola de watchers. - Según la condición
!pending, cuando hay unpending, se salta directamente la ejecución detimerFunchasta que se complete el últimonextTickHandler.
Por cierto, lo que normalmente usamos como $nextTick es en realidad lo mismo que la función nextTick:
Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this);};Resumen
Dos colas:
- La cola de
queueWatcher - La cola de
callbacksdenextTick
La cola es más flexible y conveniente para agregar componentes que necesitan modificarse de manera oportuna durante la actualización.
Aunque callbacks parece ser una cola y también es un array, cuando waiting es true, no se agregan elementos a callbacks. callbacks debería mantenerse en un estado en el que solo haya una función en espera. (En teoría, debería ser así, pero no sé cómo verificarlo, si hay algún error, por favor corrígeme).
Flujo:
- El propósito de la actualización asincrónica es evitar la ejecución repetida de la función de actualización y hacer que la ejecución sea más fluida.
- Después de que el setter activa el watcher, la función de devolución de llamada no se ejecuta de inmediato, sino que se agrega el watcher a la cola
queuea través de la funciónqueueWatcher. - Cuando está inactivo, la función
queueWatcherejecutanextTick(flushSchedulerQueue)para que laqueuese ejecute en el siguiente tick. - La función
flushSchedulerQueueejecuta las funciones de la cola de watchers. nextTickes similar a$nextTickque se usa comúnmente, es decir, coloca la ejecución de la cola en el siguiente tick.- Vue tiene 4 métodos para generar el próximo tick: setImmediate, MessageChannel, Promise, setTimeout.