Interview question: please explain the principle of vue2 response
vue official statement: cn.vuejs.org/v2/guide/re...
The ultimate goal of responsive data is to run some functions when the object itself or object properties change, the most common being the render function.
In the specific implementation, vue uses several core components:
- Observer:
- Dep
- Watcher
- Scheduler
Observer
The goal of Observer is very simple, that is, to convert an ordinary object into a responsive object
In order to achieve this, Observer converts each property of the object into a property with getter and setter through Object.defineProperty. In this way, vue has the opportunity to do something else when accessing or setting the property.
Code implementation response
/** * Define a reactive property on an Object. * Define a responsive data */ export function defineReactive ( obj: Object, // Incoming object key: string, // Object property name val: any, // The value of the object property customSetter?: ?Function, // Custom setter shallow?: boolean // No depth response ) { // Create a dependent instance object const dep = new Dep() // Gets the current property descriptor const property = Object.getOwnPropertyDescriptor(obj, key) // If the object cannot be configured, return directly if (property && property.configurable === false) { return } // Cat for pre-defined getter / setters meets the predefined getter/setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // In case of deep response, call the observe method let childOb = !shallow && observe(val); // Use Object.defineProperty to set and getter, so that you can Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // Perform dependency collection if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // When the data changes, set a new value 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() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // Notify of data changes dep.notify() } }) } Copy code
The above list is the response of a single object. In fact, if it is a nested object in the object, it is necessary to recursively traverse all the attributes of the object to complete the deep attribute conversion
Observer is the internal constructor of Vue. After Vue 2.6, we can use this function indirectly through the static method Vue. Observable (object) provided by Vue. Implementation of api
// 2.6 explicit observable API Vue.observable = <T>(obj: T): T => { // The data is returned directly after the data is expressed in a response. Since the object is passed by reference, there will be the following code observe(obj) return obj } Copy code
Concrete implementation of observe
/** * Try to create an observer instance for the value. If the observation is successful, a new observer is returned. If the value already has an observer, an existing observer is returned. */ export function observe (value: any, asRootData: ?boolean): Observer | void { // If the incoming data is not an object or a vue virtual node, it is returned directly if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // Judge whether the incoming data has__ ob__ And the prototype of value is Observer if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // Create a value instance object for value. The Observer converts the attribute key of the target object into a getter/setter, which is used to collect dependencies and send updates ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } Copy code
In the component life cycle, this happens after beforeCreate and before created.
Because only the current attributes of the object can be traversed during traversal, it is impossible to monitor the dynamically added or deleted attributes in the future. Therefore, vue provides two instance methods $set and $delete to allow developers to add or delete attributes to existing responsive objects through these two instance methods$ Implementation of set
/** * Set the properties of the object. Add a new property and trigger a change notification when the property does not exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { // Judge target object if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // If the object is an array and the subscript is a valid index (number) of an array if (Array.isArray(target) && isValidArrayIndex(key)) { // Expand array length target.length = Math.max(target.length, key) // Put data target.splice(key, 1, val) return val } // If the attribute exists in the target Object but does not exist on the prototype Object of the superclass Object if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__; // The object cannot be a Vue instance or the root data object of a Vue instance if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // ob does not exist. Add attributes directly to the object if (!ob) { target[key] = val return val } // Change the attribute to a responsive attribute defineReactive(ob.value, key, val) // The dependency notification uses the of the object for render updates ob.dep.notify() return val } Copy code
Implementation of $del
/** * Delete properties and trigger updates if necessary. */ export function del (target: Array<any> | Object, key: any) { // Like set, determine whether the target is a reference value if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } // Determine whether the target is an array and whether the key is a number if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__; // The object cannot be a Vue instance or the root data object of a Vue instance if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } // If the key does not exist in the target, it is returned directly if (!hasOwn(target, key)) { return } // Exist for deletion delete target[key] if (!ob) { return } // Notify and render ob.dep.notify() } Copy code
✨✨✨ Note: through $set and $del, we will find that although these two methods are officially provided, they should be used as little as possible. After all, we need to make a lot of judgment and then notify render.
For an array, vue changes its implicit prototype because vue needs to listen for methods that may change the contents of the array
Key code to turn an array into a response
// Determine whether there is an object prototype if (hasProto) { // By using__ proto__ Intercept the prototype chain to expand the target array protoAugment(value, arrayMethods) The above sentence is equal to value.__proto__ = arrayMethods } else { // Expand the target object or array by defining hidden properties. copyAugment(value, arrayMethods, arrayKeys) } // Turn an array into a response this.observeArray(value) Copy code
In short, the goal of Observer is to make an object, its property reading, assignment and internal array changes can be Vue perceived. Enables Vue to do something when data changes..
Dep
There are two unsolved problems: what to do when reading attributes and what to do when attributes change. This problem needs to be solved by Dep.
Dep means Dependency, which means Dependency.
Vue will create a Dep instance for each attribute, object itself and array itself in the responsive object. Each Dep instance has the ability to do the following two things:
- Record dependencies: when a property of a responsive object is read, it collects dependencies
- Distribute updates: when a property is changed, it will distribute updates
Create Dep core code for responsive objects
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value // Create a dep instance, and each response object will have a dep instance this.dep = new Dep() this.vmCount = 0 // ... turn data into responsive data } Copy code
Dep is originally a publish subscribe mode
/** * dep Is an observable object, and multiple instructions can subscribe to it */ export default class Dep { // Objective of observation static target: ?Watcher; id: number; // Collection of currently observed target objects subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // Add a subscriber addSub (sub: Watcher) { this.subs.push(sub) } // Remove subscriber removeSub (sub: Watcher) { remove(this.subs, sub) } // Collection dependency depend () { if (Dep.target) { Dep.target.addDep(this) } } // notice notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { // Let update update update the dependency subs[i].update() } } } Copy code
Watcher
There is another problem here, that is, how does Dep know who is using me?
To solve this problem, we need to rely on another thing, Watcher.
When responsive data is used in the execution of a function, it is impossible to know which function is using its own. vue gives the function to a thing called a watcher for execution. The watcher is an object. When each such function is executed, a watcher should be created and executed through the watcher. watch simplified version
The observer parses the expression, collects dependencies, and triggers a callback when the expression value changes. This is used for the $watch () api and instructions
export default class Watcher { constructor( vm: Component, expOrFn: string | Function, // The name of the property to watch cb: Function, // Callback function options?: ?Object, // configuration parameter isRenderWatcher?: boolean // Whether it is a render function observer. When Vue is initialized, this parameter is set to true ) { // Omit part of the code... The function of the code here is to initialize some variables // expOrFn can be a string or a function // When will it be a string? For example, when we use it normally, watch: {X: FN}, Vue will convert the key 'x' into a string // When will it be a function? In fact, when Vue is initialized, it is the passed in rendering function new Watcher(vm, updateComponent,...); if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // When expOrFn is not a function, it may be described in this way: Watch: {'a.x' () {/ / do}}, which is specific to the properties of an object // At this time, you need to pass the parsePath method, which returns a function // The function will get the value of the attribute 'a.x' this.getter = parsePath(expOrFn) // Omit some code } // This.get is called here, which means that this.get will be called when new Watcher // this.lazy is a modifier, which is false unless passed in by the user. You can ignore it first this.value = this.lazy? undefined: this.get() } get () { // Assign the current watcher instance to the Dep.target static attribute // That is, after this line of code is executed, the value of Dep.target is the current watcher instance // And put Dep.target on the stack and store it in the targetStack array pushTarget(this) // Omit some code try { // this.getter is executed to get the initial value of the attribute // If the updateComponent function is passed in during initialization, udnefound will be returned at this time value = this.getter.call(vm, vm) } catch (e) { // Omit some code } finally { // Omit some code // Out of stack popTarget() // Omit some code } // Returns the value of the property return value } // Here's another review // Dep.dependent method will execute Dep.target.addDep(dep), which is actually watcher.addDep(dep) // watcher.addDep(dep) will execute dep.addSub(watcher) // Add the current watcher instance to the sub array of dep, that is, collect dependencies // dep.depend and this addDep method have several this, which may be a little windy. addDep (dep: Dep) { const id = dep.id // The following two if conditions are the effect of de duplication, and we can ignore them for the time being // Just know that this method executes dep.addSub(this) if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // Add the current watcher instance to the sub array of dep dep.addSub(this) } } } // Distribute updates update () { // If the user defines lazy and this.lazy is a descriptor, we can ignore it here first if (this.lazy) { this.dirty = true // this.sync indicates whether the callback is triggered immediately after the value is changed. If the user definition is true, this.run is executed immediately } else if (this.sync) { this.run() // Internally, queueWatcher also executes the run method of the watcher instance, but internally calls nextTick for performance optimization. // It puts the current watcher instance into a queue, traverses the queue and executes the run() method of each watcher instance at the next event loop } else { queueWatcher(this) } } run () { if (this.active) { // Get new property value const value = this.get() if ( // If the new value is not equal to the old value value !== this.value || // If the new value is a reference type, be sure to trigger the callback // For example, if the old value is an object, // In the new value, we only change the value of a certain attribute in the object, and the new value is equal to the old value itself // That is, if this.get returns a reference type, the callback must be triggered isObject(value) || // Depth watch this.deep ) { // set new value const oldValue = this.value this.value = value // this.user is a flag. If the developer adds the watch option, this value defaults to true // If the watch is added by the user, add a try catch. It is convenient for users to debug. Otherwise, the callback is executed directly. if (this.user) { try { // Triggers a callback with the new and old values as parameters // This is why when we write watch, we can write: function (newval, oldval) {/ / do} 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) } } } } // Omit some code } Copy code
The Watcher will set a global variable to make the global variable record the watcher currently responsible for executing equal to itself, and then execute the function. During the execution of the function, if there is a dependency record Dep.dependent (), Dep will record the global variable. When Dep sends updates, it will notify all previous recorded watchers to update, Execute the run function
-
Each vue component instance corresponds to at least one watcher, in which the render function of the component is recorded.
-
The Watcher will first run the render function once to collect dependencies, so the responsive data used in render will record the watcher.
-
When the data changes, dep will notify the watcher, and the Watcher will re run the render function to re render the interface and re record the current dependencies.
Scheduler
The last problem that remains is that after Dep notifies the watcher, if the watcher reruns the corresponding function, it may cause the function to run frequently, resulting in low efficiency
Imagine that if attributes a, b, c and d are used in a function given to the watcher, the dependencies of attributes a, b, c and d will be recorded, so the following code will trigger four updates:
state.a = "new data"; state.b = "new data"; state.c = "new data"; state.d = "new data"; Copy code
This is obviously inappropriate. Therefore, after receiving the notification of sending updates, the watcher actually does not immediately execute the corresponding function, but gives itself to something called the scheduler
Scheduler core code
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // Judge a limit situation and whether you are joining the team if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } // Out of line queue.splice(i + 1, 0, watcher) } // queue the flush queue is refreshing if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // Put it into nextick for micro queue execution nextTick(flushSchedulerQueue) } } } Copy code
The scheduler maintains an execution queue. The same watcher in the queue only exists once. The watchers in the queue are not executed immediately. It will put these watchers to be executed into the micro queue of the event cycle through a tool method called nextTick. The specific method of nextTick is completed through Promise
nextTick is exposed to developers through this.$nextTick
For the specific processing method of nextTick, see: cn.vuejs.org/v2/guide/re...
That is, when the responsive data changes, the render function is executed asynchronously and in the micro queue
nextick core method
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { // There are two kinds of maintenance functions in the callbacks stack if (cb) { try { // Change the context of cb cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // When not waiting if (!pending) { pending = true // Execute the timerFuc function, and the implementation of this function will be determined according to the current environment. // Vue internally attempts to use native for asynchronous queues ` Promise.then`,`MutationObserver` and ` setImmediate`, // If the execution environment does not support it, the ` setTimeout(fn, 0)` Replace. timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } Copy code