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
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.
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:
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()
.
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 -
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 thatqueueWatcher
will runrun
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.
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