skip to content
usubeni fantasy logo Usubeni Fantasy

How Does Vue's Reactivity Work

/ 10 min read

This Post is Available In: CN EN ES JA

Over the years, I have read many articles on the Vue principles. With the help of these articles, I have made multiple attempts to understand the Vue source code on my own. Finally, I feel that it is time for me to share my understanding with others from a different perspective than other articles.

Naturally, this topic will be divided into multiple parts to explain the Vue source code. Let’s start with the most classic Vue reactivity principle in the first article!

Before diving into the principle, I think it is important to clarify the following concepts:

Dep

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

Dep stands for dependency. In the context of computer science, it refers to a dependency. It is similar to the dependencies we use when writing Node.js programs from the npm repository. In Vue, a dependency specifically refers to data that has undergone reactive processing. As we will discuss later, one of the key functions in reactive processing is defineReactive, which is mentioned in many articles about Vue principles. When a Dep object is bound to a reactive data, that data becomes a dependency (noun). Later, when we discuss Watchers, we will see that a reactive data can be depended upon (verb) by watch functions, computed functions, and template rendering functions.

subs

The Dep object has a subs property, which is an array. It is easy to guess that subs stands for subscribers. Subscribers can be watch functions, computed functions, or view update functions.

Watcher

Watcher is the subscriber mentioned in Dep (not to be confused with the Observer we will discuss later).

The purpose of a Watcher is to respond to updates from Dep in a timely manner. It is similar to subscribing to news updates in some apps. As a Watcher, you subscribe to certain information (Dep), and you are notified when the information is updated.

deps

Similar to Dep having a subs property, the Watcher object also has a deps property. This creates a many-to-many relationship between Watchers and Deps. The reason for recording this relationship is to ensure that when one side is cleared, the relevant objects can be updated in a timely manner.

How Watchers are created

The creation of Watchers, which we mentioned earlier in the context of watch functions, computed functions, and template rendering, is clearly demonstrated in the Vue source code:

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

Observer

Observer is responsible for recursively observing (or processing) reactive objects (or arrays). In the printed instance, you can notice that reactive objects are accompanied by a __ob__ property, which serves as proof that they have been observed. Observers are not as important as Dep and Watcher, so it is sufficient to have a basic understanding of them.

walk

Observer.prototype.walk is the core method used by Observer for recursive processing during initialization. However, this method is used for objects, and there is another method called Observer.prototype.observeArray for processing arrays.

Core Process

Based on the relationships between the concepts mentioned above, how do we implement data reactivity updates?

First, let’s define our goal: to automatically refresh the view and display the latest data when the data is updated.

This is where the relationship between Dep and Watcher comes into play. Data is represented by Dep, while Watcher triggers the page rendering function (which is the most important watcher).

However, a new question arises: how does Dep know which Watchers depend on it?

Vue uses an interesting method to solve this:

  • Before running the callback function of Watcher, first record what the current Watcher is (through Dep.target).
  • When using reactive data in the callback function, the getter function of the reactive data will definitely be called.
  • In the getter function of the reactive data, the current Watcher can be recorded, establishing the relationship between Dep and Watcher.
  • Afterwards, when the reactive data is updated, the setter function of the reactive data will definitely be called.
  • Based on the previously established relationship, the corresponding callback function of the Watcher can be triggered in the setter function.

Code

The above logic is in the defineReactive function. There are many entry points for this function, so let’s first talk about the more important observe function.

In the observe function, a new Observer object will be created, and Observer.prototype.walk will be used to perform reactive processing on each value in the object, using the defineReactive function.

Because the defineReactive function is very important and not long, it is convenient to directly paste it here.

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 the case where both the old and new 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 power of closures. (In Vue 3, closures are no longer needed.)

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

  • obj: the object where the value to be reactively processed 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 Vue’s reactivity, 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 simply adding each other to a many-to-many list, as mentioned above.

You can find all the Watchers that subscribe to the same Dep in the subs of Dep, and you can also find all the Deps that the Watcher subscribes to in the deps of the Watcher.

But there is still a hidden question, how does Dep.target come? Let’s put it aside for now, and we will answer it later.

setter

Let’s continue to look at the setter function, where the key is dep.notify().

Dep.prototype.notify = function notify() {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};

It is not difficult to understand. Dep notifies all the subscribers in its subscriber list (subs) to update. The so-called subscribers are all Watchers, and subs[i].update() calls Watcher.prototype.update.

Now let’s see what the update of the Watcher does -

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

Here I think there are two points worth expanding, so let’s dig some holes 😂

  • Hole 1: If it is not updated synchronously here, it will go to queueWatcher. We will talk about asynchronous updates later. At the same time, it also reduces the difficulty of understanding here. In short, knowing that queueWatcher will run run after a series of operations is enough.
  • Hole 2: The cb function of the Watcher may handle watch, computed, and component update functions. The component update function is particularly important, and the Vue page update is also performed here. So it is also worth expanding here. To reduce the difficulty of understanding, just know that the update is triggered here, and we will talk about the update method later.
  • Hole 3: It can be seen that when lazy is true, the following steps are not executed, only the data update is marked, and the new value is calculated when the value is retrieved next time.
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
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);
}
}
}
};

The focus of this code is on the setting of Dep.target in the get method (specifically in the path run -> get -> pushTarget).

Because only when Dep.target exists, the Dep.prototype.depend can truly take effect when the callback function cb is called (for example, the page rendering function is a typical Watcher cb). After that, the logic goes back to accessing the reactive data, and everything is connected! It forms a closed loop (funny)! This is the answer to the lingering issue of depend() mentioned above.

Summary

  • Dep is associated with data and represents that the data can be a dependency.
  • Watcher has three types: watch, computed, and rendering function. These functions can be subscribers to dependencies.
  • Observer is an entry point for handling Dep and recursively processing reactive data.
  • The callback function of Watcher sets Dep.target before using reactive data.
  • In the getter function of reactive data, the caller is identified through Dep.target, and a subscriber-dependency relationship is established.
  • In the setter function of reactive data, the subs are traversed to notify all subscribers of the data update.
  • When the subscriber is a view update function (updateComponent -> _update), users can see the page update when the reactive data is updated, thus achieving reactive update effects.

Although this algorithm is not difficult to understand in general, there are actually many other mechanisms that work together with this algorithm to form the complete Vue. For example, the update queue and the implementation of the component update function mentioned above are also worth studying.

In addition, there are more small details in the code, which I leave for those who are interested to explore on their own.

PS. Due to my limited expression ability and the curse of knowledge, I am not sure if this text can really explain the Vue reactive principle clearly. If there is anything you don’t understand, please feel free to ask in the comments. Thank you all 💡

The only reference link: Vue.js

评论组件加载中……