First, talk about the observer model
Observer pattern is a behavior design pattern to decouple one-to-many relationships. It mainly involves two roles: the observing object and the observer. As shown in figure:
The observer subscribes to the observer directly. Once the observer notifies the observer, the observer processes the notification (this is the biggest difference between the observer mode and the publish/subscribe mode).
Explanation:Some places say the observer model and
Publish/subscribe
It’s the same, it’s not exactly the same,
Publish/subscribe
In the model, its decoupling capability is a step closer,
The publisher
Just get the news out, don’t care if the news is out
The subscriber
Subscription. The observer mode requires both ends to exist
Observer mode, implemented as follows:
Constructor () {this.list = []; // Constructor () {this.list = []; } add(obj) { this.list.push(obj); } removeAt(index) { this.list.splice(index, 1); } count() { return this.list.length; } get(index) { if (index < 0 || index >= this.count()) { return; } return this.list[index]; } indexOf(obj, start = 0) { let pos = start; while (pos < this.count()) { if (this.list[pos] === obj) { return pos; } pos++; } return -1; Constructor (fn) {this.update = fn;}} class constructor(fn) {this.update = fn; }} class Subject {constructor() {this.observers = new ObserverList(); } addObserver(observer) { this.observers.add(observer); } removeObserver(observer) { this.observers.removeAt( this.observers.indexOf(observer) ); } notify(context) { const count = this.observers.count(); for (let i = 0; i < count; ++i) { this.observers.get(i).update(context); }}}Copy the code
Now, suppose we need to print the most recent value of A when data A changes, using the code above:
Const observer = new Observer((newval) => {console.log(' A's latest value is ${newval} '); }) const subject = new Subject(); subject.addObserver(observer); // Now, make the notification of the latest value change of A > subject.notify('Hello, world'); // Console output: < 'Hello, world'Copy the code
Vue and Vue dependency collection
Vue is a framework for implementing data-driven views. As we all know, Vue enables views to be refreshed when a data change occurs and changes are synchronized to other places that use the data. In addition, the data must be dependent before views and other uses of the data change. Therefore, Vue needs to be able to know whether a data is being used or not. This mechanism is implemented using a technique called dependent collection. According to the official Vue documentation, the mechanism is shown in the following figure:
– Each component instance has a corresponding Watcher instance – the process of rendering the component records properties as dependencies – When we manipulate a data, the setter for the dependency is called to inform Watcher of the recalculation, which causes the associated component to be updated
So, now the problem comes: ~~ excavator technology which is strong… If we have three data A, B, and C in the template, how can we handle the view refresh when A, B, and C change? To do this, consider the following two questions: 1. How do we know what data is used in the template? How do we tell the render() function that the data has changed?
So it’s natural to wonder if there’s a time to do something like: If you intercept a getter, you have a setter to intercept when a value changes. If you intercept a setter, you can do the next step.
So in the getter, we do dependency collection (dependencies are the data that this component needs to rely on), and when the dependent data is set, the setter gets this notification and tells the Render () function to recalculate.
Rely on the collection and observer model
As we can see, the above vUE dependent collection scenario is a one-to-many approach (one data change, multiple places using the data must be able to process), and the dependent data change, must be processed, so the observer mode is a natural solution to the problem of dependent collection. So, in Vue dependency collection: Who is the observer? Who are the targets? Obviously: – The dependent data is the object to observe – views, computed properties, listeners these are observers
Corresponding to the observer implementation code at the beginning of this article, notify actions can be done in setters and addObserver() actions can be done in getters.
4. Collect Vue dependencies from source code parsing
Let’s begin our source code parsing journey. The main read here is Vue2 early commit version, the source code is relatively concise, suitable for mastering the essence.
1, role,
Vue source code implementation of dependency collection, implementation of three classes: -dep: Subscribers (subs = subscribers) are in the queue of subs (subscribers). When data changes, call DEP.notify () to notify Watcher: Play the role of the observer and wrap the observer function. The Render () function, for example, is wrapped as a Watcher instance – Observer: an auxiliary observable class that transforms arrays/objects into observable data
2. Every data has itDep
The class instance
An instance of the Dep class is attached to each data and is used to manage instances of the Watcher class that depend on the data
let uid = 0; class Dep { static target = null; // Clever design! constructor() { this.id = uid++; this.subs = []; } addSub(sub) { this.subs.push(sub); } removeSub(sub) { this.subs.$remove(sub); } depend() { Dep.target.addDep(this); } notify() { const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}}Copy the code
Because JavaScript is a single-threaded model, there are multiple observer functions, but only one observer function is executing at a time. Then the Watcher instance of the observer function being executed at the moment is assigned to variables like dep.target. This lets you know who the current observer is simply by accessing dep. target. In subsequent dependency collection, dep.depend() is called in the getter and dep.notify() is called in the setter.
3. Configure data observation
What do we mean by saying that every data has an instance of class Dep? Before we get into data observations, let’s give a concrete example of what happens before and after processing, as shown in the following object (options.data) :
{
a: 1,
b: [2, 3, 4],
c: {
d: 5
}
}Copy the code
After configuring the data observation, it looks like this:
Dep => Dep (uid:0) a: 1, Dep (uid:1) b: => Dep (uid:0) a: 1, Dep (uid:1) b: [2, 3, 4], / / there is dep in the closure (uid: 2), and b. __ob__. Dep = > dep (uid: 4) c: Dep => Dep (uid:5) d:5 // Dep (uid:6)}}Copy the code
The new Observer class starts with the Component constructor that produces each Component. In the Component constructor, a series of actions are performed before the Component is instantiated.
this._ob = observe(options.data)
this._watchers = []
this._watcher = new Watcher(this, render, this._update)
this._update(this._watcher.value)Copy the code
Observe (options.data), huh? Observer? Observe in lower case? Afraid is not the object that fights xi Xi to buy? First let’s see what the observe function does:
function observe (value, vm) { if (! value || typeof value ! == 'object') { return } var ob if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! value._isVue) { ob = new Observer(value) } if (ob && vm) { ob.addVm(vm) } return ob }Copy the code
To summarize, only one instance of an Observer class is instantiated for an object/array, only once, and only when the data is configurable. So what does the Observer class do? Take a look at the following source code:
class Observer { constructor(value) { this.value = value this.dep = new Dep() def(value, '__ob__', this) if (isArray(value)) { var augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } walk(obj) { var keys = Object.keys(obj) for (var i = 0, l = keys.length; i < l; i++) { this.convert(keys[i], For (var I = 0, l = items.length; var I = 0, l = items. i < l; i++) { observe(items[i]) } } convert(key, val) { defineReactive(this.value, key, val) } addVm(vm) { (this.vms || (this.vms = [])).push(vm) } removeVm(vm) { this.vms.$remove(vm) } }Copy the code
– Mount an instance of an Observer class on an __ob__ property to provide subsequent observation data and to avoid repeated instantiation. Then, instantiate the Dep class instance and save the object/array as a value property – if value is an object, walk() is performed to traverse the object making each item observable (call defineReactive) – if value is an array, To perform the observeArray() procedure, recursively call observe() on the array elements so that you can handle the element as an array
4. How to observe arrays?
Object.defineproperty () does have some problems handling arrays. We can use an example to illustrate this:
const data = { arr: [1, 2, 3] } function defineReactive(obj, key, val) { const property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return; } const getter = property && property.get; const setter = property && property.set; Object. DefineProperty (obj, key, {enumerable: true, different: true, get() {console.log(' cx is blocked '); const value = getter ? getter.call(obj) : val; return value; }, set(newval) {console.log(' new value is ${newval} ') if (setter) {setter.call(obj, newval); } else { val = newval; } } }) } defineReactive(data, 'arr', data.arr);Copy the code
We then ran a set of tests with the following results:
data.arr; Data. arr[0] = 1; // The value process is blocked data.arr.push(4); // The value process is blocked data.arr.pop(); // The value process is blocked data.arr.shift(); // The value process is blocked data.arr.unshift(5); // The value process was intercepted data.arr.splice(0, 1); Data.arr.sort ((a, b) => a-b); Data.arr.reverse (); Data. Arr = [4, 5,6] // The new value is 4,5,6Copy the code
As you can see, except for reassigning an array to arR, nothing else is detected by setters. So to detect array changes, Vue does the following when an array item is passed in:
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)Copy the code
This enhancement actually detects array changes by defining a set of methods on the array’s prototype chain. That is, defining a set of prototype methods on the object to which arr.__proto__ points. If the browser does not support __proto__, Mount it directly on the array object itself), and then observe the array item. So how does enhancement detect array changes? Then we need to use the idea of AOP, which is to keep the original operation and embed our specific operation code. Here’s an example:
const arrayMethods = Object.create(Array.prototype); Prototype const originalPush = arrayMethods.push; Object.defineProperty(arrayMethods, 'push', { configurable: true, enumerable: false, writable: true, value(... args) { const result = originalPush.apply(this, args); Console. log(' Push array, add value: ', args); return result; }}) data.arr.__proto__ = arrayMethods data.arr.push([5, 6], 7Copy the code
So, by doing this for each array manipulation method, we have a way to notify the observer when the array changes. Vue is implemented as follows:
; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { var original = arrayProto[method] def(arrayMethods, method, function mutator () { var i = arguments.length var args = new Array(i) while (i--) { args[i] = arguments[i] } var result = original.apply(this, args) var ob = this.__ob__ var inserted switch (method) { case 'push': inserted = args break case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) ob.dep.notify() return result }) })Copy the code
The idea is still the same: – Keep the array’s original operations – push, unshift, splice, etc., bring in new elements that we know are passed in – then the new elements need to be configured as observable so that subsequent changes can be processed. Call observeArray (observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray
5, Watcher
Watcher plays the role of an observer. It cares about the data, gets notified when it changes, and acts on it. A component can have multiple instances of the Watcher class. The Watcher class wraps the Watcher function, which uses data. – Template render: this._watcher = new Watcher(this, render, this._update) – calculate attributes:
computed: { name() { return `${this.firstName} ${this.lastName}`; New Watcher(this, function name() {return '${this.firstname} ${this.lastname}'}, callback); * /Copy the code
We pass in the component instance, the observer function, the callback function, and the option. Then we explain four variables: deps, depIds, newDeps, and newDepIds. -depids: stores deP instances used by the observer function in this round. -newdeps: stores DEP instances used by the observer function in this round. -newdepids: stores the deP instances used by the observer function in this round
Get (); get(); get(); In the initial preparation, the current Watcher instance is assigned to dep. target and newDeps and newDepIds are emptied. Because defineReactive() is performed in the data observation phase, the data used in the calculation is accessed, triggering the getter for the data to execute the watcher.adddep () method, AddDep (dep) is executed for each data. If the deP does not exist in newDeps, it will be added to newDeps. This is because data can be used more than once during a calculation, but the same dependency can only be collected once. In addition, if dePS does not exist, it means that the current watcher did not depend on a certain data in the last round of calculation, nor does the current Watcher exist in the corresponding DEP. subs of that data. Therefore, the current Watcher should be added to the dep.subs of the data. Deps = newDeps; deps = deps; deps = deps; Accordingly, remove the current watcher from the dep.subs corresponding to the data – assign newDeps to DEPS to cache the calculation results of this round, so that the next round of calculation does not need to collect the same data again
8. When a data is updated, the setter intercepts the watchers in the dep.subs observer queue of the data, so the watcher.update() method is executed. The update() method repeats the evaluation process (i.e. Steps 3-7). The result of the recalculation of the observer function, render(), synchronizes the view with the latest data
6, defineReative
As we all know, Vue implements data hijacking using Object.defineProperty() and intercepting data using Object.defineProperty() is encapsulated in defineReactive. DefineReactive () : defineReactive()
function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
var getter = property && property.get
var setter = property && property.set
var childOb = 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 (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}Copy the code
The get/set method in object.defineProperty () forms a closure relative to var dep = new dep (), which neatly saves the DEP instance. If an observer function accesses some data, we can consider the observer function to be dependent on the data, so a concrete example: data.a, is used in the following places:
<template> <div>{{a}}</div> </template> computed: { newValue() { return this.a + 1; }}Copy the code
The template is then compiled to form an AST that fires the getter for data.a during render() and is lazily collected (newValue uses a, but does not fire the getter if it is not called). It will not be added to the dep.subs of data.a) now, suppose template looks like this:
<template> <div>I am {{a}}, plus 1 is {{newValue}}</div> </template>Copy the code
So, you can see that there are two observer functions: the compute property newValue and the render() function, which are wrapped as two Watcher functions. When render() is executed, data.a is accessed, so that render@watcher is added to the dep.subs of data.a and newValue is accessed, and data.a is accessed. Add newValue@watcher to dep.subs of data.a. So the dep.subs of data.a has [render@watcher, newValue@watcher]. Why is watcher added to the deps.subs of data when accessing specific data? This is because you are already in the context of a Watcher before accessing the getter, so one thing is guaranteed: An instance of the Watcher class Watcher is ready and has called watcher.get(). Dep.target has a value so we see that the getter for dependency collection is dep.depend() and no arguments are passed in because, We just need to add dep. target to the current dep.subs. Dep.prototype.depend();
depend() {
Dep.target.addDep(this);
}Copy the code
Why not add dep. target directly to dep.subs with Depend () instead of calling dep.target.adddep? This is because we can’t just mindlessly stuff the current watcher into the DEP. subs, we want to make sure that each watcher in the dep.subs is unique. Dep.target is an instance of the Watcher class. Calling dep.depend() is equivalent to calling the watcher.adddep method, so let’s look at what this method does:
Watcher.prototype.addDep = function (dep) { var id = dep.id if (! this.newDepIds[id]) { this.newDepIds[id] = true this.newDeps.push(dep) if (! this.depIds[id]) { dep.addSub(this) } } }Copy the code
To sum up, determine if the dependency is collected in this round, and then stop collecting it, and add newDeps if it is not collected. At the same time, determine whether there is too much cache dependency, cache is no longer added to the DEP. subs.
3. Setters tell Watcher to recalculate when the value changes. Since the setter has access to the deP in the closure, it can get the DEP. subs and know which watcher depends on the current data. If the setter changes its value, it can call dep.notify() to traverse the watcher in the dep.subs. Execute the update() method for each watcher to make each watcher recalculate.
7. Confusion point analysis
Returning to the previous example, we said that the option.data of the example, after being observed, becomes:
{
__ob__, // dep(uid:0)
a: 1, // dep(uid:1)
b: [2, 3, 4], // dep(uid:2), b.__ob__.dep(uid:3)
c: {
__ob__, // dep(uid:4), c.__ob__.dep(uid:5)
d: 5 // dep(uid:6)
}
}Copy the code
We wonder why two instances of the Dep class are instantiated after configuring dependent observations for arrays and objects. For reference type data, there are two operations: change the reference and change the content, namely:
data.b = [4, 5, 6]; // Change the reference data.b.ush (7); // Change the contentCopy the code
In fact, changing references, as we mentioned earlier about the object.defineProperty () limitation, can be detected, so the DEP in the closure can collect this dependency. Object defineProperty() does not detect changes in the contents, so the array mutation operation is encapsulated, so the array needs to be attached to __ob__ property, __ob__ deP instance, to handle changes in the contents, so that the trace link can be formed.
Third, summary
In summary, Vue’s dependency collection is an application of the observer pattern. Its principle is summarized as follows: