Write in the front words: about the author Yan Chuan
My pen name is Yanchuan, front-end engineer, proficient in Vue/Webpack/Git, familiar with Node/React, etc., dabble in a wide range of fields, including algorithm/back end/artificial intelligence/Linux, etc. Open source enthusiast, currently 5000+ Star on Github.
- My dead simple homepage: https://github.com/lihongxun945
- My blog: https://github.com/lihongxun945/myblog
- My Denver home page: https://juejin.cn/user/1398234518660551
- My zhihu column: https://zhuanlan.zhihu.com/c_1007281871281090560
This blog to the original address: https://github.com/lihongxun945/myblog/issues/27
fromcomputed
Speaking of
In order to understand Watcher we need to choose a pointcut, and this time we’re going to use computed as the pointcut. This is a very common feature, and it explains how we detect state changes and get the latest value. Let’s assume that we have the following components:
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'}},computed: {
upperMsg () {
return this.msg.toUpperCase()
}
}
}
Copy the code
We have data. MSG and compute. upperMsg for both custom data. Obviously, upperMsg depends on MSG, and when MSG is updated, upperMsg is also updated. From the previous chapter, we know that we can monitor reads and writes to MSG using an Observer. How does this relate to upperMsg?
Watcher is the key to connecting the two. Let’s see how the initWatcher code works. The complete code is as follows:
core/observer/watcher.js
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if(process.env.NODE_ENV ! = ='production' && getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if(! isSSR) {// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if(! (keyin vm)) {
defineComputed(vm, key, userDef);
} else if(process.env.NODE_ENV ! = ='production') {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); }}}}Copy the code
For convenience, let’s remove some friendly warnings in the development environment and remove some code that does not affect our logic. Let’s look at the code:
We look at the code line by line, skipping for the sake of convenience some friendly warnings in a development environment and a few lines that don’t interfere with our logic and understanding of the meaning of the code.
First, the first two lines of code:
var watchers = vm._computedWatchers = Object.create(null);
var isSSR = isServerRendering();
Copy the code
These two lines of code define two variables, Watchers is an empty object, which is obviously used to store the next watchers created, and isSSR indicates whether or not the watchers are rendered on the server side, because if it’s rendered on the server side, there’s no need to listen, let’s leave the server side aside.
Next is a for loop that traverses a computed object. The first section of the body of the loop looks like this:
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if(! isSSR) {// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
Copy the code
The getter here is our upperMsg function, but it handles the case that we defined with the getter. Once we have the getter, we will create a Watcher for each key we define. Here’s the main point. Let’s jump into the watcher constructor for a moment, in the file core/observer/watcher.
Further Watcher kind
The complete constructor code is as follows:
constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync }else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {} process.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
Although the code is a bit long, most of the code is the initialization of some properties, the more important ones are:
lazy
If you set it totrue
The first timeget
The value is computed at the time of initialization, not at the time of initialization. The default value istrue
deps
.newDeps
.depIds
.newDepIds
Recording dependencies, that’s what we’re going to focus onexpOrFn
Our expression itself
In addition to setting these properties, there is only the last line of code:
this.value = this.lazy
? undefined
: this.get()
Copy the code
Note that the design of this. Value, Vue design, Watcher will not only listen to the Observer, but he will directly calculate the value on this. Lazy is not evaluated directly here, but it is evaluated when it is evaluated, so let’s look at the getter code directly:
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\" "));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
` `Here we see something familiar`pushTarget`Function, but instead of clearing it this time, I'm actually clearing it`this`As a parameter, so the result is`Dep.target === this`. Forget about this one. I'll just`pushTarget`The code is posted again:` ``js Dep.target = null const targetStack = [] export function pushTarget (_target: ? Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target }Copy the code
When we take the upperMsg value, the global dep. target becomes the watcher instance corresponding to upperMsg. Then we can directly value:
value = this.getter.call(vm, vm)
Copy the code
So we execute the upperMsg function and get the uppercase string of MSG. And in the getter, we have code like this.msg that reads MSG, so it jumps into the getter in defineReactive.
Let’s review our code in defineReactive again:
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
/ / to omit}}Copy the code
Dep.target is the watcher instance we created for upperMsg, so the dep.depend() function is executed. This function looks like this:
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); }};Copy the code
Target is watcher, so this line of code is equivalent to watcher.adddep (Dep). Let’s look at the addDep function:
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); }}};Copy the code
When addDep is executed, it’s going to save the dep, but it’s going to have the two arrays that were initialized earlier, deps and newDeps, and the two sets of depIds and newDepIds. And you can see that this is obviously a deweighting, especially when depIds and newDepIds are a Set.
But the logic for deduplicating is a bit complicated, because it contains two ifs, one for depIds and one for newDepIds. So why do it twice? As an example, let’s first assume that we have a computational property like this:
computed: {
doubleMsg () {
return this.msg + this.msg
}
}
Copy the code
MSG is evaluated twice, so obviously the getter is fired twice, and the dep.depend() call in the getter does not determine any repetition conditions, So in order to calculate a doubleMsg, you enter the watcher.prototype.adddep function twice. The second entry is ignored because newDepIds already records the ID of the DEP instance. So why does the second entry have the same DEP as the first entry? Because dep is in the closure outside the getter/setter, it is unique to the current MSG.
If (newDepIds); if (newDepIds); We first take a look at where to use the newDepIds, actually is the Watcher protototype. CleanupDeps function, and this function is the Watcher. Prototype. Get called, Let’s look at finally in get code:
finally {
/ / to omit
this.cleanupDeps();
}
Copy the code
CleanupDeps. This function assigns the value of newDepIds to depIds, and then clears newDepIds.
When Vue evaluates doubleMsg, this. MSG is called twice, and when the evaluation is complete, the this.cleanupDeps operation is performed. When the evaluation is complete, our dependency is in depIds instead of newDepIds. Once you know that, it’s easier to understand. NewDepIds simply avoids multiple dependencies on MSG during the evaluation of doubleMsg. When the evaluation is complete, newDepIds is empty, and the dependency is recorded in depIds. What if we evaluated doubleMsg again after we evaluated it the first time? Like this:
mounted () {
this.msg = 'aaaa'
}
Copy the code
If you assign this. MSG after $mount, the watcher.update method is triggered, and this. MSG is evaluated again. At this point, newDepIds is empty and depIds has a value, so it is not dependent on repeated records.
So the bottom line is:
newDepIds
Can be found inupperMsg
During the evaluation process ofmsg
Repeated dependence ofdepIds
Can be due tomsg
Update and cause to re-pairdoubleMsg
When you evaluate it, you don’t want to be rightmsg
Repeated dependence of
Once you’ve figured out how to redo the code, the main line of code is dep.addsub (this). That is, watcher will be added to dep.subs.
So far, we’ve been able to trigger dependency collection once we call this.upperMsg to read the value. How does watcher.value know when MSG is updated? Again, look at the setter definition in defineReactive:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() }Copy the code
The most important of these is the last line of code, dep.notify, which notifies all watcher with the following notify code:
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Copy the code
It will call watcher.update to update the value, so that when we set a new value to the MSG, watcher.value will be updated automatically. Because of performance issues, the watcher.update function is updated asynchronously by default. Let’s look at the code:
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke((a)= > {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
There’s a lot of comments in there, and the first few lines deal with when there are other values that depend on our upperMsg, which we’ll talk about later, but we’ll skip over here. Look directly at the last few lines of code:
if (this.sync) {
this.run()
} else {
queueWatcher(this)}Copy the code
In sync mode, run is called directly to update the value. By default it is asynchronous, so the queueWatcher(this) method is entered, deferring the run until nextTick. This is why we read upperMsg immediately after updating the MSG and the content has not been updated. Because all updates are concentrated on nextTick, Vue will perform better. QueueWatcher is relatively simple. It records all operations in a queue and is called once on nextTick. I’m not going to go into that, we’ll have a separate chapter on that.
Now that we know how upperMsg depends on MSG, I’ll draw a diagram to tease out the relationship between them:
To explain this graph, the blue line is the reference relationship (except for the line between Observer and DEP, because that line is actually a closure and not a reference), and the red line is the triggering process of the dependency.
- We’re through
this.msg = xxx
To modify themsg
Of the value he wasobserver
Listen, so the observer knows that this update has occurred - There is one in the Observer
dep
If the dependency is logged, he will calldep.notify
To inform those subscribers - Dep. subs saves the subscribers and calls them
update
methods - Call the
watcher.update
Method, which will eventually be called after several timesnextTick
Update whenthis.value
The value of the
Back to initComputed
Going back to our original initComputed function, we’ve seen so much about how new Watcher works, and this function has one last piece of code:
if(! (keyin vm)) {
defineComputed(vm, key, userDef);
}
Copy the code
The defineComputed function acts as a proxy for upperMsg on this, so we can access it via this.uppermsg. DefineComputed code is as follows:
export function defineComputed (target: any, key: string, userDef: Object | Function) {
constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if(process.env.NODE_ENV ! = ='production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`.this)}}Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
It will set this.upperMsg via object.defineProperty, which is still defined by getter/setter, and the reads and writes to this. UpperMsg will be propended to the upperMsg we specified in options.
So far we have thoroughly understood how responsive works by interpreting data and computed. Props won’t be expanded here because it involves VDOM, but the responsive part of its implementation is the same as data.