Data detection is an important part of Vue, React, and Angular frameworks. Data detection is how to synchronize user input data or data obtained from the back end to the view through some mechanism. UI=render(state) Render is the rendering function and UI is the view that is finally rendered to the user. As soon as the state is updated, the render function is triggered to rerender the new view. How does Vue detect changes in data and trigger view rendering?
The observer pattern defines a one-to-many dependency that allows multiple observer objects to listen to a target object at the same time. When the state of the target object changes, all observer objects are notified so that they can update automatically.Copy the code
The data detection mechanism of Vue adopts observer mode. When the view is bound to the data object data, how does Vue listen to the data update? Vue2 uses Object DefineProperty function to realize data update detection. This function can define get function to get value and set function to set value. We can customize logic in get and set function.
Object.defineProperty(obj, key,
{ enumerable: true, configurable: true,
get: function reactiveGetter () {},
set: function reactiveSetter (newVal) {}
})
Copy the code
Vue defines a unified Observer Observer object. All objects will be converted into objects containing get and set functions through the Observer, and all observers will be collected in get and set functions. Data changes will be synchronized to all observers. The constructor in the code is used to receive the data Object and determine its type. Since Array updates the Array through push and unshift functions and cannot be listened through getters and setters, the Observer treats Object and Array types differently. Next, see how Vue handles Object and Array objects.
export class Observer { value: any; constructor (value: any) { this.value = value if (Array.isArray(value)) { this.observeArray(value) } else { this.walk(value) } } /** * */ walk (obj: Object) {} /** * observeArray type */ observeArray (items: Array<any>) {}}Copy the code
The Observer includes the Walk function for handling Object objects, which iterates through all the keys of the Object and calls defineReactive to update the response. DefineReactive calls Object.defineProperty to convert all attributes of the data Object to get and set modes, all values, the initial val passed in, or subsequent values set through set, The observe function is called to convert it to an Observer. With get and SET, we can attach observers to the process, and notify each observer of the corresponding status update after the data update triggers the set.
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; I ++) {defineReactive(obj, keys[I])}} / / export function defineReactive(obj: Object, key: string, val: any ) { // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set let childOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val return value }, set: function reactiveSetter (newVal) { if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = ! shallow && observe(newVal) } }) }Copy the code
Defines observeArray function Observer handle Array type, function traversal data elements, and call the observe function processing each element separately, function to determine whether a value contains __ob__ attribute, if contain explain the value has been successfully converted to the Observer object, can be directly to return. So when is the __ob__ attribute attached to the value? Vue defines the __ob__ attribute in the Observer constructor call def(value, ‘ob’, this). Observe converts the value to an Observer, and the Observer recurses downward through all properties, eventually converting all property values to an Observer.
*/ observeArray (items: Array<any>) {for (let I = 0, l = items.length; i < l; I ++) {observe(items[I])}} /** * Try to convert the property value to an observer, * If the property value is not an observer, new a new observer; * If the property value is of type Obserer, the object is returned directly; */ export function observe (value: any): Observer | void { if (! isObject(value) || value instanceof VNode) { return } let ob: The Observer | void / / if the value contains __ob__ object representation has been converted to the Observer object if (hasOwn (value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! value._isVue ) { ob = new Observer(value) } return ob }Copy the code
Since the Array contains push, splice, and unshift functions that are not heard by getters and setters, the Observer handles Array objects separately. The Observer constructor adds 9 to 13 lines of code to preprocess arrays. If the browser supports the __proto__ attribute, arrayMethods can be overridden by assigning the __proto__ attribute directly. Otherwise, call copyAugment directly to override the operand function of the array.
export class Observer { value: any; constructor (value: any) { this.value = value def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, ArrayKeys)} this.observearray (value)} else {this.walk(value)}}} /** * extend the prototype chain of the target object with __proto__ attribute */ function ProtoAugment (target, SRC: Object) {target.__proto__ = SRC} /** * extend the target Object by defining hidden attributes */ function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }Copy the code
The array function that Vue overwrites is defined in arrayMethods, and the function that WE’re overwriting is defined in the methodsToPatch array, and we’re going to iterate over that array and override the original array with a custom function, The custom function listens for the newly inserted value by calling the OB. observeArray function. Splice (start[, deleteCount[, item1[, Item2 [,…]]]]), the first and second arguments indicate the number of rows to be deleted. So we have to rule it out.
Const arrayProto = array. prototype export const arrayMethods = object.create (arrayProto) /** ** arrayProto */ const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', ForEach (function (method) {// cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (... args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) return result }) })Copy the code
So far we have seen how Vue encapsulates Object and Array objects, but we have not yet covered how to listen and notify. In dep.js, Vue defines deP objects as target objects, which can be listened by multiple Watcher. The list of observers is stored in subs Array. Add and remove observers via addSub and removeSub. The latest Watcher is stored on the global dep. target. Wacher’s addDep function is executed when the Depend function is called, which in turn appends the latest Watcher to the subs by calling this.dep.adSub(this). Notification is triggered by the notify function.
/ / export default class Dep {static target:? Watcher; id: number; subs: Array<Watcher>; Constructor () {this.id = UI ++ this.subs = []} /** ** add () {constructor () {this.id = UI ++ this.subs = []} Watcher) {this.subs.push(sub)} /** * remove the observer * @param {observer} sub */ removeSub (sub: Watcher) {remove(this.subs, sub)} /** * Attach the latest observer to the deP If (dep.target) {dep.target.adddep (this)}} if (dep.target) {dep.target.adddep (this)}} Call each Watcher's update function to perform the update. */ notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }Copy the code
With Dep objects defined, how does Vue trigger registration and notification through an Observer? The DEP attribute is first defined in the Observer constructor to listen on the current Observer object.
export class Observer { value: any; constructor (value: any) { this.value = value this.dep = new Dep() ... }}Copy the code
Object is registered through defineProperty’s get function. The code adds the registration logic in lines 15 to 18. The latest observation Object is temporarily stored in the DEP. target Object. The dep. target(Watcher) is registered in the dep.subs array by executing dep. Depend. In addition, if the object’s subproperty values change (e.g. person.age = 30), the observer will also need to listen. The current dep.target is registered with the child property value Observer by executing the cildob.dep. depend function. The get function determines the type of the value. If the value is an Array object, the dependArray function is invoked to iterate over each element. The e.o.Dep. depend function ensures that each element is monitored by dep. target. A recursive call to the dependArray function iterates recursively over each element of the array, ensuring that it is listened on by the observer.
/ / export function defineReactive (obj: Object, key: string, val: any) {const dep = new dep ()... let childOb = ! shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : Val if (dep.target) {dep.depend() if (childOb) { // For example, the {person: {age: 20}} object, Watcher needs to listen for both person changes and age changes. childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: Function reactiveSetter (newVal) {}}) $dependArray (value: $dependArray) $dependArray (value: $dependArray (value: $dependArray) $dependArray (value: $dependArray (value: $dependArray (value: $dependArray)) Array<any>) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } }Copy the code
When a new value is assigned to an object, the set function is triggered. The last line of this function calls dep.notify, loops through the Watcher object in subs and executes this.subs[I].update to notify the object. For array types, add ob.dep.notify() to the operator function of the methodsToPatch array to notify the array of the change.
export function defineReactive (...) {
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {},
set: function reactiveSetter (newVal) {
...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
//array.js文件
methodsToPatch.forEach(function (method) {
...
// notify change
ob.dep.notify()
return result
})
})
Copy the code
So far, Vue has introduced how to implement Object and Array detection and update. It can be linked together with the following flow chart, including four classes Observer, Dep, Array, Watcher, and four independent functions marked in red.
Execution process description: 1. Walk: Call defineReactive converts val to an object containing getters and setters. 2. ObsererArray: Call observeArray iterates through each element of the array Rewrite :Array (' push ', 'splice', 'unshift'); rewrite:Array (' push ', 'splice', 'unshift'); DependArray 6. Recursive array elements may also be arrays. Get: Depend Append Watcher to val 9. Set :notify set:notify Call Watcher's addDep function from Dep's Depend function to attach a listener. Dep :addSub Add Watcher to the subs array by calling DEP's addSub in the addDep functionCopy the code
The Watcher object is mentioned in the process but not described in detail. When writing Vue components, we often use Watch to listen for property changes. For example, when firstName and lastName properties change, the fullName property is automatically updated.
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
},
'baseInfo.phone': function(val) {
console.log('new phone num: ${val}')
}
}
Copy the code
Watcher plays the role of observer in the entire state flow. Vue attribute changes are eventually notified to Watcher. Watcher supports monitoring of chained attribute changes (such as Baseinfo.phone), and Watcher triggers the re-rendering of the view. The next section will show how Vue listens for changes and synchronizes them to components in an orderly manner, combined with the Watcher. js and scheduler.js files in the Vue source code.
Appendix:
1. Differences between published subscriber and observer modesSegmentfault.com/a/119000002…
2. The plain ObjectNewbedev.com/the-differe…
3.Object’s __proto__ attribute,Developer.mozilla.org/zh-CN/docs/…