summary
We all know that Vue's responsiveness is to perform data hijacking. But that can be implemented for Object types. What if it is an array? It is not possible to use set/get method.
But the Vue authors used a way to implement Array type monitoring: an interceptor.
Core idea
Overwrite the prototype object of the array itself by creating an interceptor.
Interceptor
Check the Vue source code path vue/src/core/observer/.
/** * Vue's change detection of arrays * Thought: Overwrite with an interceptor. * Interceptor is actually an Object, and its properties are the same. It only processes the variation method of the array. */ function def (obj, key, val, enumerable) { (obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } // Array prototype objectconst arrayProto = // Interceptorconst arrayMethods = (arrayProto) // Mutation array method: The method that changes the original array after executionconst methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] (function (method) { // Methods on cached original array prototypes const original = arrayProto[method] // Process each array compilation method (intercept) def(arrayMethods, method, function mutator (...args) { // The returned value is still the result of execution through the array prototype method itself const result = (this, args) // When each value is observer(), it will be marked with a __ob__ attribute const ob = this.__ob__ // Store the array that calls to execute the mutated array method to cause the value of the array itself to change, mainly referring to the part of the original array that increases (re-Observer is required) let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = (2) break } //Re-Observe newly added array elements if (inserted) (inserted) // Send change notification () return result }) })
About when to use Observer on data properties in Vue
If you are familiar with Vue source code, you should be able to find the Vue entry file vue/src/core/instance/ soon.
function Vue (options) { if (.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) // Bind the proxies attribute $props, $data//Bind three instance methods to Vue prototype: vm.$watch, vm.$set, vm.$deletestateMixin(Vue) // Example methods related to binding events to Vue prototype: vm.$on, vm.$once, vm.$off, vm.$emiteventsMixin(Vue) // Bind Vue prototype life cycle related instance methods: vm.$forceUpdate, and private method_updatelifecycleMixin(Vue) // Bind Vue prototype life cycle related instance methods: vm.$nextTick, and private method_render, and a bunch of tool methodsrenderMixin(Vue) export default Vue
()
Source code path: vue/src/core/instance/.
export function initMixin (Vue: Class<Component>) { ._init = function (options?: Object) { // Current instance const vm: Component = this // a uid // Instance unique identifier vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ // Development mode, enable Vue performance detection and API-enabled browsers. if (.NODE_ENV !== 'production' && && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` // Start placing in the component initialization stage mark(startTag) } // a flag to avoid this being observed // Identified as a Vue instance vm._isVue = true // merge options // Pass the optionsMerge into $options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(), options || {}, vm ) } /* istanbul ignore else */ if (.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // Initialize life cycle initLifecycle(vm) // Initialize event center initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props // Initialize State initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (.NODE_ENV !== 'production' && && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // Mount if (vm.$) { vm.$mount(vm.$) } } }
initState()
Source code path: vue/src/core/instance/.
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if () initProps(vm, ) if () initMethods(vm, ) if () { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if () initComputed(vm, ) if ( && !== nativeWatch) { initWatch(vm, ) } }
At this time you will find that the observe appears.
observe
Source code path: vue/src/core/observer/
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // The value is already a responsive data and no Observe instance is created to avoid repeated listening ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && ((value) || isPlainObject(value)) && (value) && !value._isVue ) { // The target appears, create an Observer instance ob = new Observer(value) } if (asRootData && ob) { ++ } return ob }
Time to use interceptor
There is an Observe class in Vue's responsive system. Source code path: vue/src/core/observer/.
// can we use __proto__? export const hasProto = '__proto__' in {} const arrayKeys = (arrayMethods) function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } function copyAugment (target: Object, src: Object, keys: Array<string>) { // target: Objects that need to be Observed // src: Array proxy prototype object // keys: const arrayKeys = (arrayMethods) // keys: Several compilation method names on the array proxy prototype object // const methodsToPatch = [ // 'push', // 'pop', // 'shift', // 'unshift', // 'splice', // 'sort', // 'reverse' // ] for (let i = 0, l = ; i < l; i++) { const key = keys[i] def(target, key, src[key]) } } export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { = value // = new Dep() = 0 def(value, '__ob__', this) // If it is an array if ((value)) { if (hasProto) { // If __proto__ attribute (non-standard attribute, supported by most browsers): Directly point the prototype to the proxy prototype object protoAugment(value, arrayMethods) } else { // It is not supported to mount the processed variant method of the same name on the array instance (and cannot be enumerated) to intercept prototype object method // When you access an object method, you will only look up on the prototype object when it does not exist. copyAugment(value, arrayMethods, arrayKeys) } (value) } else { (value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = (obj) for (let i = 0; i < ; i++) { defineReactive(obj, keys[i]) } } /** * Iterate through each item in the array to listen for changes, that is, each element executes Observer() once. */ observeArray (items: Array<any>) { for (let i = 0, l = ; i < l; i++) { observe(items[i]) } } }
How to collect dependencies
The real data responsive processing in Vue is defineReactive(). defineReactive method is to convert the object's data attribute into the accessor attribute, that is, to set get/set for the data attribute.
function dependArray (value: Array<any>) { for (let e, i = 0, l = ; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.() if ((e)) { dependArray(e) } } } export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // dep is used in the accessor attribute closure // Each data field refers to its own dep constant through a closure // Dep objects for each field are used to collect those dependencies that belong to the corresponding field. const dep = new Dep() // Get the property description object that may already exist in this field const property = (obj, key) // Boundary situation handling: An unconfigurable property cannot be used and there is no need to use it to change its attribute definition. if (property && === false) { return } // Since an object's property is likely to be an accessor property, it is likely that the property already has a get or set method // If the function is used next to redefine the setter/getter of the property // This will cause the original set and get methods of the attribute to be overwritten, so the original setter/getter of the attribute to be cached const getter = property && const setter = property && // Boundary situation handling if ((!getter || setter) && === 2) { val = obj[key] } // The default is depth observation, referring to the sub-properties __ob__ // Provides trigger dependencies for the or method. let childOb = !shallow && observe(val) (obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // If getter exists, then call the function directly and use the return value of the function as the value of the attribute to ensure that the original reading operation of the attribute is normal // If getter does not exist, use val as the value of the property const value = getter ? (obj) : val // The value is assigned when instantiating the Watch if () { // Start collecting dependencies to dep () if (childOb) { () if ((value)) { // Call the dependArray function to trigger the dependency collection of each element of the array one by one dependArray(value) } } } // Return the property value correctly. return value }, set: function reactiveSetter (newVal) { // Get the original value const value = getter ? (obj) : val /* eslint-disable no-self-compare */ // Compare whether the old and new values are equal, consider the NaN situation if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return // If there is a setter before the data, then you should continue to use this function to set the value of the property if (setter) { (obj, newVal) } else { // Assign new values val = newVal } // Since the property is set a new value, then if the new value we set for the property is an array or a pure object, // Then the array or pure object is not observed, so the new value needs to be observed childOb = !shallow && observe(newVal) // Notify the watcher update in dep () } }) }
List of array dependencies
Why do we need to place dependencies on Observer instances? Right now
export class Observer { constructor (value: any) { ... = new Dep() } }
First we need to access the Observer instance in getter
// That is, the abovelet childOb = !shallow && observe(val) ... if (childOb) { // Call the depend() method of dep on the Observer instance to collect dependencies () if ((value)) { // Call the dependArray function to trigger the dependency collection of each element of the array one by one dependArray(value) } }
In addition, we need to use the Observer instance in the interceptor mentioned above.
(function (method) { ... // this indicates the data currently being operated // But how did __ob__ come from? const ob = this.__ob__ ... //Re-Observe newly added array elements if (inserted) (inserted) // Send change notification () ... })
Think about where the above-mentioned this.__ob__ attribute comes from?
export class Observer { constructor () { ... = new Dep() // Add a non-enumerable __ob__ attribute on vue, the value of this attribute is the Observer instance // Therefore, we can obtain the Observer instance through array data __ob__ // Then get the dep on __ob__ def(value, '__ob__', this) ... } }
Remember that once all attributes are detected, they will be marked with a __ob__ tag, which means they are responsive data.
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.