In recent years, I have read a lot of articles about the mechanism of Vue2.0. With their help, I pore over the source code of Vue. And now, I think it's time to export something about it by myself. I hope this post can help you to understand Vue more deeply from a different perspective.
In the first post, we will learn about the most elaborate design—reactivity.
Before we dive into the core code, we should understand those conceptions ↓
Dep
var Dep = function Dep() {
this.id = uid++
this.subs = []
}
Dep
means dependency. Like writing a node.js program, we will use dependencies on npm. In Vue, what we depend is the data which is reactive. I will soon mention the core function of Vue reactivity—defineReactive
.
A reactive data becomes a dependency when it binds with a dep.
subs
Dep object has a subs property, which is an array. As we guess, it's a list of subscribers. There are three kinds of "watcher"—watch
, computed
, and render function.
Watcher
Watcher
is the subscriber of Dep
(don't mix it with the Observer).
Dep's update can be responded to by Watcher. It's like you subscribe to a YouTube channel(Dep), you(Watcher) will immediately watch the new clip right after you get the update notification.
deps
The function of deps
is similar to that of subs
(from Dep
). This design provide a many to many relationship between Watcher
and Dep
. While one of Watcher
or Dep
is removed, the other one will be updated.
How to create Watcher
There are three kinds of "watcher", which have been mentioned before. We can find how they are created in the source code.
mountComponent
的vm._watcher = new Watcher(vm, updateComponent, noop);
initComputed
的watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
$watcher
的var watcher = new Watcher(vm, expOrFn, cb, options);
Observer
The responsibility of Observer
is to "observe" data or array recursively. When you log the Vue instance in the console, you may notice that there is a __ob__
property in reactive data, which is the proof of "observed".
walk
Observer.prototype.walk
is the core function that Observer
uses to process objects. For arrays, Observer.prototype.observeArray
is used.
Reactivity processing
Now that we understand those conceptions mentioned above, how can we achieve reactivity with them?
Set our goal first: When reactive data is updated, user can see the newest data (Of course, this process should be automatic.)
That's easy, as we know, reactive data is Dep
and the render function (what user can see is generated by this function) is Watcher
(and It's the most important Watcher
).
But the question is, How Dep
know which Watcher
is depending it?
The interesting thing comes:
- Record the current watcher (in
Dep.target
) before running the callback function of it. - The getter function of the reactive data, which is used by the watcher, must be triggered.
- The Current Watcher is recorded by the getter function, and the relationship between Dep and Watcher is constructed.
- When we update reactive data, the setter function of it must be triggered.
- Base on the relationship constructed before, the related Watcher callback function can be triggered in the setter function.
Source code
In fact, the process above is implemented by the defineReactive
function. This function is invoked in many places. Let's see the most important—observe
function.
observe
function create Observer
object, then process data using defineReactive
in Observer.prototype.walk
.
defineReactive
is important and concise, so I 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
// NaN situation
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if ('development' !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
},
})
}
Every single property of a reactive object is a "dependency" so the first step is to create a Dep
for it by using a closure. (closure will not be needed in Vue3)
Look at the parameters:
- obj: the object waiting for reactivity processing
- key
- val: current value, which may be defined a getter and a setter
getter
Previous I mention that the getter function constructs the relationship between Dep
and Watcher
, and more precisely is achieved by dep.depend()
.
Below is some function about the interoperation of Dep
and Watcher
:
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)
}
It seems intricate between Dep
and Watcher
, but what those functions actually do is to create the many to many relationship.
You can find all subscribers of a Dep
in its subs
property, and all Dep
s that a Watcher
is watching in its deps
property.
There is a question hidden here—where is Dep.target
set? Don't hurry; I'll tell you later.
setter
The Key function in the setter function 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's not hard to understand. Dep
notify its subscribers (in subs
) to update. subs[i].update()
invoke Watcher.prototype.update
.Let's see what it does:
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
There are two points I think we can dive into:
- If it updates asynchronously,
queueWatcher
will run. But it eventuallyrun
. - The sequence of running
watch
,computed
, and render function is worth noticing. - The
lazy
flag is used forcomputed
values.
I may talk about them in the next few posts.
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 important thing of this code clip is that Dep.target
is set here (run -> get -> pushTarget).
Because of the existence of Dep.target
, Dep.prototype.depend
invoked by Watcher cb
makes sense. That's the answer to the unsolved question.
Wrap up
- An object bind with a
Dep
, and it become a dependancy. watch
,computed
, and render functions areWatcher
s, and they can be subscribers of dependancy.Observer
is an entry to process reactive data recursively.Watcher
will setDep.target
before running callback function.- The getter function of reactive data perceive its "Caller" from
Dep.target
and construct relationship withWatcher
- The setter function of reactive data traverse its
subs
and notify them to run their update function. - While the
Watcher
runs render function (updateComponent
->_update
), user can see the newest data on the web page.
Though understanding the fundamental algorithm is not so hard, there are still many details this post doesn't mention yet. For example, the update queue and the component update function itself are worth studying.
I hope you enjoy this post!