skip to content
usubeni fantasy logo Usubeni Fantasy

Análisis del principio de reactividad de Vue

/ 10 min read

This Post is Available In: CN EN ES JA

En los últimos años he leído muchos artículos sobre los principios de Vue, y con la ayuda de estos artículos, he intentado varias veces entender el código fuente de Vue por mí mismo. Finalmente, creo que es hora de compartir mi conocimiento y espero poder familiarizar a todos con Vue desde una perspectiva diferente a la de otros artículos.

Naturalmente, este tema se dividirá en varias partes para explicar el código fuente de Vue. Empezaremos con el principio más clásico de Vue: la reactividad.

Antes de entrar en los detalles del principio, creo que es importante aclarar algunos conceptos:

Dep

var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};

Dep significa “dependency” (dependencia en inglés), que es un término utilizado en el campo de la informática.

Es similar a cuando escribimos programas en node.js y usamos las dependencias del repositorio npm. En Vue, una dependencia se refiere específicamente a los datos que han sido procesados de manera reactiva. Más adelante mencionaremos una de las funciones clave en el procesamiento reactivo, que es defineReactive, que se menciona en muchos artículos sobre los principios de Vue.

Cuando se vincula un objeto Dep a un dato reactivo, ese dato se convierte en una dependencia. Más adelante, cuando hablemos de Watcher, veremos que un dato reactivo puede ser dependencia de tres tipos de funciones: watch, computed y la función de renderizado de la plantilla.

subs

El objeto Dep tiene una propiedad llamada subs, que es un array. Es fácil adivinar que subs significa “subscribers” (suscriptores en inglés). Los suscriptores pueden ser funciones watch, funciones computed o funciones de actualización de la vista.

Watcher

El Watcher es el “suscriptor” mencionado en Dep (no confundir con el Observer que mencionaremos más adelante).

La función del Watcher es responder rápidamente a las actualizaciones de Dep, similar a cómo funcionan las notificaciones de suscripción en algunas aplicaciones. Tú (el Watcher) te suscribes a cierta información (Dep) y se te notifica cuando esa información se actualiza para que puedas leerla.

deps

Al igual que el objeto Dep tiene la propiedad subs, el objeto Watcher tiene la propiedad deps. Esto crea una relación de muchos a muchos entre Watcher y Dep. La razón por la que se registran mutuamente es para poder actualizar los objetos relacionados cuando uno de ellos se elimina.

Cómo se crea un Watcher

En el código fuente de Vue, se puede ver claramente cómo se crean los Watchers que hemos mencionado varias veces:

  • En mountComponent, se crea vm._watcher = new Watcher(vm, updateComponent, noop);
  • En initComputed, se crea watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
  • En $watcher, se crea var watcher = new Watcher(vm, expOrFn, cb, options);

Observer

El Observer es el “observador” que se encarga de observar (o procesar) de forma recursiva los objetos (o arrays) reactivos. Si observas las instancias que se imprimen, notarás que los objetos reactivos tienen una propiedad __ob__, que es una prueba de que han sido observados. El Observer no es tan importante como el Dep y el Watcher, así que solo necesitamos tener una idea general de su funcionamiento.

walk

Observer.prototype.walk es el método principal que se utiliza para procesar de forma recursiva los objetos cuando se inicializa el Observer. Además de este método, también existe Observer.prototype.observeArray, que se utiliza para procesar los arrays.

Flujo principal

Teniendo en cuenta la relación entre los conceptos mencionados anteriormente, ¿cómo podemos lograr la actualización reactiva de los datos?

Nuestro objetivo es actualizar automáticamente la vista y mostrar los datos más recientes cuando los datos se actualicen.

Aquí es donde entra en juego la relación entre Dep y Watcher. Los datos son las Dep y el Watcher es el que desencadena la función de renderizado de la página (que es el Watcher más importante).

Pero surge una nueva pregunta: ¿cómo sabe Dep qué Watcher depende de él?

Vue utiliza un método muy interesante para resolver esto:

  • Antes de ejecutar la función de devolución de llamada de Watcher, se guarda la información de qué Watcher es actualmente (a través de Dep.target).
  • Si se utiliza datos reactivos en la función de devolución de llamada, se llamará a la función getter de los datos reactivos.
  • En la función getter de los datos reactivos se puede guardar la información del Watcher actual y establecer la relación entre Dep y Watcher.
  • Después, cuando los datos reactivos se actualicen, se llamará a la función setter de los datos reactivos.
  • Basado en la relación establecida anteriormente, se puede activar la función de devolución de llamada correspondiente del Watcher en la función setter.

Código

La lógica anterior se encuentra en la función defineReactive. Esta función tiene varias entradas, pero aquí se explicará primero la función observe, que es más importante.

En la función observe, se crea un nuevo objeto Observer y se utiliza Observer.prototype.walk para realizar el procesamiento reactivo de los valores del objeto uno por uno, utilizando la función defineReactive.

Debido a que la función defineReactive es muy importante y no es muy larga, se muestra directamente aquí para mayor comodidad.

function defineReactive(obj, key, val, customSetter, shallow) {
var dep = new Dep()
depsArray.push({ dep, obj, key })
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
// The second part of the condition is used to determine if both the new and old values are NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
// customSetter is used to remind you that the value you set may have problems
if ("development" !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});

First, each property of the reactive object is a “dependency”, so the first step is to create a Dep for each value using the closure capability. (In Vue 3, closures are no longer needed.)

Next, let’s look at the three core parameters:

  • obj: the object where the value that needs to be reactive is located
  • key: the key of the value
  • val: the current value

This value may have its own getter and setter defined, so when processing the reactivity of Vue, we first handle the original getter and setter.

Getter

In the core process mentioned above, it is mentioned that the Dep and Watcher relationship is established in the getter function, specifically relying on dep.depend().

Here are a few methods that Dep and Watcher call each other:

Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};

Through these functions, you can see the complex relationship between Dep and Watcher… But in essence, it is just adding each other to a many-to-many list, as mentioned above.

Puedes encontrar todos los Watchers que se suscriben al mismo Dep en subs de Dep, y también puedes encontrar todos los Dep a los que se suscribe el Watcher en deps de Watcher.

Pero hay una pregunta oculta, ¿cómo se obtiene Dep.target? Lo dejaremos por ahora y lo responderemos más adelante.

setter

Continuemos viendo la función setter, donde se encuentra la clave dep.notify().

Dep.prototype.notify = function notify() {
// estabilizamos primero la lista de suscriptores
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};

No es difícil de entender, simplemente Dep notifica a todos los suscriptores en su lista (subs) para que se actualicen. Los suscriptores son Watchers, y lo que se llama es Watcher.prototype.update.

Veamos qué hace update en el Watcher:

Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};

Aquí veo dos puntos que vale la pena mencionar, así que voy a hacer una pausa 😂

  • Punto 1: Si no se actualiza de forma sincrónica, se encola en queueWatcher. Hablaré más adelante sobre la actualización asíncrona, pero esto también facilita la comprensión aquí. En resumen, solo necesitas saber que queueWatcher ejecutará run después de una serie de operaciones.
  • Punto 2: La función cb del Watcher puede manejar watch, computed y funciones de actualización del componente. Es especialmente importante la función de actualización del componente, ya que es aquí donde se actualiza la página de Vue. Así que también vale la pena mencionarlo, pero para facilitar la comprensión, solo necesitas saber que la actualización se desencadena aquí y hablaré más adelante sobre los métodos de actualización.
  • Punto 3: Puedes ver que cuando está en modo lazy, no se ejecutan los pasos siguientes, solo se marca que los datos se han actualizado y se calculará un nuevo valor la próxima vez que se acceda a ellos.
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Los watchers profundos y los watchers de Object/Arrays deben ejecutarse incluso
// cuando el valor es el mismo, porque el valor puede haber mutado.
isObject(value) ||
this.deep
) {
// establecer nuevo valor
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, 'callback for watcher "' + this.expression + '"');
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

El punto clave de este código es que se ha establecido Dep.target en el método get actual. (La ruta específica es run -> get -> pushTarget)

Porque solo cuando Dep.target existe, Dep.prototype.depend se activará realmente cuando se llame a la función de devolución de llamada cb (por ejemplo, la función de renderización de la página es un ejemplo típico de Watcher cb). A continuación, la lógica vuelve a utilizar el valor de los datos reactivos, ¡todo está conectado! ¡Formando un bucle cerrado (risas)! Esta es la respuesta al problema pendiente de depend() mencionado anteriormente.

Resumen

  • Dep está asociado con los datos y representa que los datos pueden ser dependencias.
  • Watcher tiene tres tipos: watch, computed y función de renderización. Estas funciones pueden ser suscriptores de las dependencias.
  • Observer es una especie de punto de entrada para manejar Dep, procesando datos reactivos de forma recursiva.
  • La función de devolución de llamada de Watcher establece Dep.target antes de utilizar los datos reactivos.
  • Los datos reactivos en la función getter conocen al llamador a través de Dep.target y establecen una relación entre suscriptores y dependencias.
  • Los datos reactivos en la función setter notifican a todos los suscriptores en subs que los datos se han actualizado.
  • Cuando el suscriptor es una función de actualización de la vista (updateComponent -> _update), el usuario puede ver la actualización de la página cuando los datos reactivos se actualizan, logrando así el efecto de actualización reactivo.

Aunque en general este algoritmo no es difícil de entender, en realidad hay muchos otros mecanismos que trabajan junto con este algoritmo para formar un Vue completo. Por ejemplo, las colas de actualización y la implementación de la función de actualización del componente también son dignas de estudio.

Además, hay más detalles en el código que pueden ser explorados por aquellos interesados.

PD. Debido a mis habilidades de expresión que no son muy buenas y la maldición del conocimiento, no estoy seguro de si este texto realmente explica claramente el principio de reactividad de Vue. Si hay algo que no se entienda, por favor, háganlo saber en la sección de comentarios. Gracias a todos 💡

Único enlace de referencia: Vue.js

评论组件加载中……