How is the data in data processed?
Each instantiation of a component calls initData and invokes the observe method. The observe method calls new Observer(value) and returns to the ob__ .
Two things are done in new Observer:
- Mount the current instance to the of the data__ ob__ Property, this instance is useful later.
- It is handled differently according to the data type (array or object)
If object:
Traverse the object attributes horizontally and call defineReactive;
Recursively call the observe method, and stop recursion when the attribute value is not an array or an object
The defineReactive method is commented in detail below:
export function defineReactive( obj: Object, key: string, val: any, customSetter ? : ? Function, shallow ? : boolean ) { const dep = new Dep(); // Closures create dependent objects; Each object's attribute has its own dep // The following is for the object that has passed Defineproperty or object seal Object. Freeze processed data const property = Object.getOwnPropertyDescriptor(obj, key); // If configurable is false, select object again Defineproperty (obj, key) will report an error and will not succeed; So go straight back // So you can use object Free / seal optimizes performance. if (property && property.configurable === false) { return; } const getter = property && property.get; const setter = property && property.set; // Normally, the data getter and setter do not exist, and the middle note in new Observer() has only two parameters. if ((!getter || setter) && arguments.length === 2) { val = obj[key]; // In other words, this line of code will generally be executed } // Generally, share is false; childOb is the returned Observer instance, which is stored in the database__ ob__ On properties // let childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val; // getter does not exist, use val directly if (Dep.target) { // Dep.target is a global data. It stores the watcher at the top of the watcher stack (targetStack), dep.depend(); // The closure dep collects the current watcher; The collection dependency really occurs when the render method is executed (that is, when the virtual dom is generated) if (childOb) { // val is not an object (not Array or object). Only the observe method will return an Observer instance, otherwise it will return undefined // Why execute childob here What about dep.depend()? // The effect of this is: mount on the object__ ob__ The dep object adds the pre point watcher to the dependency. This dep is not the same as the closure Dep. // The purpose is to: // 1. Target: to think about this$ Set/$del can trigger the component to be rendered again, and save the middle note watcher, then call ob. in $set. dep.notify(); It's used here__ ob__ attribute // 2. For arrays: in the interception of arrays (call the methods such as splice push) to trigger re rendering, call ob Dep.notify() is used here__ ob__ attribute childOb.dep.depend(); if (Array.isArray(value)) { // If value is an array, the array program is used in the observe method, and these elements are not used by object Defineproperty is a series of processing (elements are treated as val). Even if the element is object/array, there is no childob A process such as dep.depend() causes this$ Set / $del, array cannot trigger re rendering; // Therefore, we call dependArray to process the array, which is used here__ ob__ attribute dependArray(value); } } } return value; }, set: function reactiveSetter(newVal) { // Generally, there is no getter const value = getter ? getter.call(obj) : val; // Value unchanged, newval== newVal && value !== Value should be for NaN if (newVal === value || (newVal !== newVal && value !== value)) { return; } if (process.env.NODE_ENV !== "production" && customSetter) { customSetter(); } // Getters and setter s should be paired if (getter && !setter) return; if (setter) { setter.call(obj, newVal); } else { val = newVal; } // After resetting the value, you need to re observe and update the closure variable childOB childOb = !shallow && observe(newVal); // to update dep.notify(); }, }); }
If array:
Modify the of the array__ proto__ Attribute value, pointing to a new object;
function protoAugment (target, src: Object) { target.__proto__ = src }
Redefine the following methods in this new object:
'push','pop','shift','unshift','splice','sort','reverse'
At the same time, this object's__ proto__ Point to array prototype.
const arrayProto = Array.prototype; export const arrayMethods = Object.create(arrayProto);
Finally, the code of the project prints the following screenshot on the console
data() { return { data1: [{ name: 1 }] } },

At the same time, observe recursively for each element in the array.
How is the watch option handled?
The usage of watch is generally as follows:
watch: { a: function(newVal, oldVal) { console.log(newVal, oldVal); }, b: 'someMethod', c: { handler: function(val, oldVal) { /* ... */ }, deep: true }, d: { handler: 'someMethod', immediate: true }, e: [ 'handle1', function handle2(val, oldVal) {}, { handler: function handle3(val, oldVal) {}, } ], 'e.f': function(val, oldVal) { /* ... */ } }
The key codes of watch are listed according to the following process:
-- > initData() // Called when initializing the component. If there is a watch option in the component, call initWatch -- > initWatch() if (Array.isArray(handler)) { // Here is the case of processing arrays, that is, the case of e above for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } -- > createWatcher() if (isPlainObject(handler)) { // Compatible C (object) options = handler handler = handler.handler } // In the case of b string, you need to have corresponding data on vm if (typeof handler === 'string') { handler = vm[handler] } // The default is a (function) vm.$watch(expOrFn, handler, options) -- > vm.$watch() options.user = true // Add parameter options User = true, handle the case of immediate:true const watcher = new Watcher(vm, expOrFn, cb, options) -- > new Watcher() // Create a watcher this.getter = parsePath(expOrFn) // This getter method is mainly to get the variable of watch, trigger dependency collection in the process of getting, and add the current watcher to the dependency this.value = this.lazy // The option lazy is false ? undefined : this.get() // Call the get method directly in the constructor -- > watcher.get() pushTarget(this) // Push the current watcher to the top of the stack value = this.getter.call(vm, vm) // At this time, the current watcher is included in the dependency of the watch variable -- > watcher.getter() // Place dependent on collection
When the variable of watch changes, the run method of watcher will be executed:
run() { if (this.active) { const value = this.get() // In the case of rendering the watcher, value is undefined // In the case of custom watcher, value is the listening value if ( value !== this.value || // When the value of watch changes isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value // The user of the custom watcher is true, and cb is the handler 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) } } } }
Value in the above code== this. Value and deep are easy to understand, and the change of value triggers the handler;
But what does isObject(value) correspond to? Just look at the following example:
data() { return { data1: [{ name: 1 }], } }, computed: { data2() { let value = this.data1[0].name // return this.data1 // The returned is an array, so data2 consistency is unchanged } }, watch: { data2: function() { // Although the value of data2 has always been data1, it has not changed; However, because data2 satisfies isObject, the handler can still be triggered // It is conceivable that we can take the initiative to obtain a data attribute in calculated to trigger the watch, and avoid using deep in the watch // However, this is not appropriate, because you can directly use the example of 'e.f' instead; // Therefore, it should be determined according to the actual situation console.log('data2'); } } created() { setInterval(() => { this.data1[0].name++ }, 2000) }
How is computed data processed?
computed first creates a watcher, which is different from rendering watchers and custom watchers: the get method will not be executed during initialization, that is, dependency collection will not be done.
In addition, use object Defineproperty defines the get method:
function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { // lazy=true, then dirty is also true if (watcher.dirty) { watcher.evaluate(); // Add the calculated watcher to the dependencies of all variables involved; } if (Dep.target) { watcher.depend(); // Actively call the dependent method; If this computed is used for page rendering, the render watcher will be added to the dependency of the variable } return watcher.value; } }; }
When the computed data is in the initial rendering:
-- > render // Render -- > computedGetter // computed Object.defineProperty defines the get method: -- > watcher.evaluate() // Calculate the watcher value -- > watcher.get() -- > pushTarget(this) // Push the current computed watcher to the top of the watcher stack -- > watcher.getter() // getter method is the method defined by computed in the component, and dependency collection will be done during execution -- > dep.depend() // Add the current computed watcher to the dependency of the variable -- > popTarget() // Put the current The computed watcher removes the stack. Generally speaking, the rendered Watcher will be pushed to the top of the stack -- > cleanupDeps() // Clear redundant watcher s and DEPs -- > watcher.depend() // This is a special place for computed. If the calculated depends on the data in the variable data, this step adds the current watcher to the dependence of the variable; Why do you do this? My guess is that the purpose of computed is to be a bridge for processing data. The real response is still the data that needs to be implemented in the data.
When the dependent data in computed changes, the following process will be followed:
-- > watcher.update() // This is a computed watcher, where lazy is true, so it won't go down if (this.lazy) { this.dirty = true } -- > watcher.update() // The process after rendering watcher render is the same as the first rendering
How to render the watcher?
Render watcher is relatively easy to understand
New watcher - > watcher get-> pushTarget(this) ->watcher. getter()-> render -> Object. defineProperty(get) -dep.depend()-> popTarget()->watcher. cleanupDeps()
watcher.getter is the following method:
updateComponent = () => { vm._update(vm._render(), hydrating) }
Dependency cleanup in Vue?
The source code is in Vue / SRC / core / observer / watcher JS;
It should be noted that vue has a scheme to clear watcher and dep; Dependency collection in vue is not one-time. Re render will trigger a new dependency collection. At this time, invalid watchers and DEPs will be removed, so as to avoid invalid updates.
Calculated as follows: once temp < = 0.5, changing b will no longer print temp; The reason is that when temp < 0.5, this b will not put the current a into its own dep, so it will not trigger the calculated monitor again
data() { return { b: 1 } }, computed: { a() { var temp = Math.random() console.log(temp); // Once a < = 0.5, temp will not be printed if (temp > 0.5) { return this.b } else { return 1 } } }, created() { setTimeout(() => { this.b++ }, 5000) },
There are mainly watchers The cleanupDeps method in JS is processing;
cleanupDeps() { let i = this.deps.length // Traverse the last saved deps while (i--) { // i-- const dep = this.deps[i] // newDepIds is the new depId set added in this dependency collection // Clear dep not in newDepIds if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // depIds is a es6 set set that refers to class data // newDepIds is similar to a temporary storage place. Finally, you need to save the data to depIds. Left hand to right hand trick // newDeps and newDepIds are the same let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
vuex response principle?
Depending on the response principle of Vue itself, the collection of dependencies is completed in the render process by constructing a Vue instance.
store._vm = new Vue({ data: { $$state: state, // Custom state data }, computed, });
Vue router reactive processing?
Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { this._routerRoot = this; this._router = this.$options.router; this._router.init(this); // The key is this line of code_ The route attribute is processed responsively Vue.util.defineReactive( this, "_route", this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } registerInstance(this, this); }, destroyed() { registerInstance(this); }, }); Object.defineProperty(Vue.prototype, "$route", { get() { return this._routerRoot._route; }, });
The above will be triggered when rendering router view
// render method when rendering this component render() { ... // Dependency collection is triggered when $route is called var route = parent.$route; }