skip to content
usubeni fantasy logo Usubeni Fantasy

El mecanismo de actualización asíncrona de Vue.

/ 10 min read

This Post is Available In: CN EN ES JA

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 desarrollo
var queue = []; // Cola de watchers
var activatedChildren = [];
var has = {}; // Indica si ya se ha agregado a la cola
var waiting = false; // Se puede entender como si se está esperando el proceso de flushing
var flushing = false; // Indica si ya se ha comenzado a procesar la cola
var index = 0; // Indica en qué posición de la cola se encuentra actualmente

Es 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:

  • setImmediate
  • MessageChannel, se puede utilizar en IE 10 o superior, es muy sorprendente, no lo sabrías si no lo buscas, el alcance de MessageChannel es incluso mayor que el de Promise.
  • 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, cb se inserta en callbacks, 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 un pending, se salta directamente la ejecución de timerFunc hasta que se complete el último nextTickHandler.

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 callbacks de nextTick

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 queue a través de la función queueWatcher.
  • Cuando está inactivo, la función queueWatcher ejecuta nextTick(flushSchedulerQueue) para que la queue se ejecute en el siguiente tick.
  • La función flushSchedulerQueue ejecuta las funciones de la cola de watchers.
  • nextTick es similar a $nextTick que 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.
评论组件加载中……