Let's look at the picture first to understand the general process and what to do
initialization
During the initialization of new Vue, the data props and data of our components will be initialized. Since this article mainly introduces the response type, I won't explain it too much. Let's take a look at the source code
Source address: Src / core / instance / init.js - line 15
export function initMixin (Vue: Class<Component>) { // Add on Prototype_ init method Vue.prototype._init = function (options?: Object) { ... vm._self = vm initLifecycle(vm) // Initialize instance properties and data: $parent, $children, $refs, $root_ Watcher... Wait initEvents(vm) // Initialization events: $on, $off, $emit, $once initRender(vm) // Initialize rendering: render, mixin callHook(vm, 'beforeCreate') // Call lifecycle hook function initInjections(vm) // Initialize inject initState(vm) // Initialize component data: props, data, methods, watch, computed initProvide(vm) // Initialize provide callHook(vm, 'created') // Call lifecycle hook function ... } }
Initialization here calls many methods. Each method does different things. The response type is mainly the data props and data in the component. The content of this block is in the initState() method, so let's go to the source code of this method and have a look
initState()
Source address: Src / core / instance / state.js - line 49
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // Initialize props if (opts.props) initProps(vm, opts.props) // Initialize methods if (opts.methods) initMethods(vm, opts.methods) // Initialize data if (opts.data) { initData(vm) } else { // If there is no data, it will be assigned to an empty object by default and listen observe(vm._data = {}, true /* asRootData */) } // Initialize computed if (opts.computed) initComputed(vm, opts.computed) // Initialize watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
To call a bunch of initialization methods again, let's go straight to the topic and take the data related to our responsive data, that is, initProps(), initData(), observe()
One by one, you have to understand the whole process of response
initProps()
Source address: Src / core / instance / state.js - line 65
The main tasks here are:
- Traverse the props list passed in by the parent component
- Verify the name, type, default attribute, etc. of each attribute. If there is no problem, call defineReactive to set it to responsive
- Then use proxy() to proxy the attributes to the current instance, such as VM_ If props.xx becomes vm.xx, you can access it
function initProps (vm: Component, propsOptions: Object) { // props passed from parent component to child component const propsData = vm.$options.propsData || {} // The final props after conversion const props = vm._props = {} // Store the props key. Even if the props value is empty, the key will still be in it const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // Convert props for non root instances if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) // Verify props type, default attribute, etc const value = validateProp(key, propsOptions, propsData, vm) // In a non production environment if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn(`hyphenatedKey Is a reserved property and cannot be used as a component prop`) } // Set props to responsive defineReactive(props, key, value, () => { // Warn if the user modifies props if (!isRoot && !isUpdatingChildComponent) { warn(`Avoid direct changes prop`) } }) } else { // Set props to responsive defineReactive(props, key, value) } // Proxy attributes that are not on the default vm to the instance // You can make VM_ Props.xx is accessed through vm.xx if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }
initData()
Source address: Src / core / instance / state.js - line 113
The main tasks here are:
- Initialize a data and get the keys collection
- Traverse the keys collection to determine whether it has the same name as the property name in props or the method name in methods
- If there is no problem, proxy every attribute in the data to the current instance through proxy(), and you can access it through this.xx
- Finally, call observe to listen to the whole data
function initData (vm: Component) { // Get the data of the current instance let data = vm.$options.data // Determine the type of data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn(`The data function should return an object`) } // Gets the data property name collection of the current instance const keys = Object.keys(data) // Get props of current instance const props = vm.$options.props // Gets the methods object of the current instance const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] // Judge whether the methods in methods exist in props in non production environment if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn(`Method Method cannot be declared repeatedly`) } } // Judge whether the attributes in data exist in props in non production environment if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn(`Property cannot be declared repeatedly`) } else if (!isReserved(key)) { // Proxy to vm without duplicate names // You can make VM_ Data.xx is accessed through vm.xx proxy(vm, `_data`, key) } } // Monitor data observe(data, true /* asRootData */) }
observe()
Source address: Src / core / observer / index.js - line 110
This method is mainly used to add a listener to the data
The main tasks here are:
- If it is an object type of vnode or not a reference type, it will jump out directly
- Otherwise, add an Observer to the data without adding an Observer, that is, a listener
export function observe (value: any, asRootData: ?boolean): Observer | void { // If it is not an 'object' type or an object type of vnode, it will be returned directly if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // Using cached objects 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 listener ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
Observer
Source address: Src / core / observer / index.js - line 37
This is a class that converts normal data into observable data
The main tasks here are:
- Mark the current value as a responsive attribute to avoid repeated operations
- Then determine the data type
- If it is an object, it traverses the object and calls defineReactive() to create a responsive object
- If it is an array, traverse the array and call observe() to listen for each element
export class Observer { value: any; dep: Dep; vmCount: number; // Number of VMS on the root object constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // Add to value__ ob__ Property, an Observe instance with value // The expression has become a response. The purpose is to skip directly when traversing the object to avoid repeated operations def(value, '__ob__', this) // Type judgment if (Array.isArray(value)) { // Determine whether the array has__ proty__ if (hasProto) { // If so, override the array method protoAugment(value, arrayMethods) } else { // If not, define the property value through def, that is, Object.defineProperty copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } // If object type walk (obj: Object) { const keys = Object.keys(obj) // Traverse all properties of the object and turn it into a responsive object. It is also a dynamic addition of getter s and setter s to achieve two-way binding for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } // Listener array observeArray (items: Array<any>) { // Traverse the array and listen for each element for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
defineReactive()
Source address: Src / core / observer / index.js - line 135
The purpose of this method is to define a responsive object
The main tasks here are:
- Initialize a dep instance first
- If it is an object, call observe and listen recursively to ensure that it can become a responsive object no matter how deep the structure is nested
- Then call Object.defineProperty() to hijack the getter and getter of object properties.
- If the getter is triggered during acquisition, it will call dep.dependent () to push the observer into the dependent array subs, that is, dependency collection
- If the setter is triggered during update, the following operations will be performed
- The new value does not change or there is no direct jump out of the setter property
- If the new value is an object, observe() is called to listen recursively
- Then call dep.notify() to distribute updates.
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // Create dep instance const dep = new Dep() // Get the property descriptor of the object const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // Get custom getter s and setter s const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // If val is an object, it will listen recursively // By calling observe recursively, you can ensure that no matter how deep the object structure is nested, it can become a responsive object let childOb = !shallow && observe(val) // Intercepting getter s and setter s of object properties Object.defineProperty(obj, key, { enumerable: true, configurable: true, // Intercept getter, and this function will be triggered when taking value get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // Perform dependency collection // When initializing the render watcher, access the object that needs two-way binding, which triggers the get function if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, // Intercept setter, which will be triggered when the value changes set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // Determine whether there is a change if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // Accessor property without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // If the new value is an object, listen recursively childOb = !shallow && observe(newVal) // Distribute updates dep.notify() } }) }
As mentioned above, dep.depend is used for dependency collection. Dep is the core of the whole getter dependency collection
Dependency collection
Dep is the core of dependency collection, and it is also inseparable from Watcher. Let's take a look
Dep
Source address: src/core/observer/dep.js
This is a class, which is actually a kind of management of Watcher
Here, first initialize a sub array to store dependencies, that is, observers who depend on this data are in this array, and then define several methods to add, delete, notify and update dependencies
In addition, it has a static attribute target, which is a global Watcher, which also means that only one global Watcher can exist at the same time
let uid = 0 export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // Add observer addSub (sub: Watcher) { this.subs.push(sub) } // Remove observer removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { // Call Watcher's addDep function Dep.target.addDep(this) } } // Distribute updates (described in the next chapter) notify () { ... } } // Only one observer is used at the same time. Assign an observer Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
Watcher
Source address: src/core/observer/watcher.js
Watcher is also a class, also called observer (subscriber). The work done here is quite complex, and it is also connected with rendering and compilation
Let's look at the source code first, and then go through the whole process of dependency collection
let uid = 0 export default class Watcher { ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // Array of Dep instances held by Watcher instance this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.value = this.lazy ? undefined : this.get() if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } } get () // This function is used to cache Watcher // Because when a component contains nested components, the Watcher of the parent component needs to be restored pushTarget(this) let value const vm = this.vm try { // Call the callback function, that is, upcateComponent, to evaluate the object that needs two-way binding, so as to trigger dependency collection value = this.getter.call(vm, vm) } catch (e) { ... } finally { // Depth monitoring if (this.deep) { traverse(value) } // Restore Watcher popTarget() // Cleaning up unwanted dependencies this.cleanupDeps() } return value } // Called on dependency collection addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // Push the current watcher into the array dep.addSub(this) } } } // Clean up unnecessary dependencies (below) cleanupDeps () { ... } // Called when distributing updates (below) update () { ... } // Execute callback of watcher run () { ... } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } }
Supplement:
-
Why can we automatically get the new value and the old value of the watch written in our own component?
The callback will be executed in watcher.run(), and the new and old values will be passed -
Why initialize two Dep instance arrays
Because Vue is data-driven, every time the data changes, it will re render, that is, the vm.render() method will re execute and trigger the getter again. Therefore, it is represented by two arrays: the newly added Dep instance array newDeps and the last added instance array deps
Dependency collection process
There will be such logic when the first rendering is mounted
mountComponent source code address: Src / core / instance / lifecycle.js - line 141
export function mountComponent (...): Component { // Call lifecycle hook function callHook(vm, 'beforeMount') let updateComponent updateComponent = () => { // Call_ update patch es (that is, Diff) the virtual DOM returned by render to the real dom. This is the first rendering vm._update(vm._render(), hydrating) } // Set an observer for the current component instance and monitor the data obtained by the updateComponent function, as described below new Watcher(vm, updateComponent, noop, { // When triggering updates, it will be called before updating. before () { // Judge whether the DOM is in the mounted state, that is, it will not be executed when rendering and uninstalling for the first time if (vm._isMounted && !vm._isDestroyed) { // Call lifecycle hook function callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // There is no old vnode, indicating that it is the first rendering if (vm.$vnode == null) { vm._isMounted = true // Call lifecycle hook function callHook(vm, 'mounted') } return vm }
Dependency collection:
- Before mounting, a render Watcher will be instantiated. After entering the watcher constructor, this.get() method will be executed
- Then it will execute pushTarget(this), that is, assign Dep.target to the current render watcher and push it onto the stack (for recovery)
- Then execute this.getter.call(vm, vm), that is, the updateComponent() function above, where VM. _update (VM. _render()) is executed
- Then execute vm._render() to generate a rendered vnode. In this process, the data on the vm will be accessed, and the getter of the data object will be triggered
- Every getter of object value has a dep. when the getter is triggered, the dep.depend() method will be called and Dep.target.addDep(this) will be executed
- Then, some judgments will be made here to ensure that the same data will not be added multiple times. Then, push the qualified data into the subs. At this point, the collection of dependencies has been completed, but it has not been completed yet. If it is an object, it will recurse the object to trigger getter s of all children, and restore the Dep.target state
Remove subscription
To remove a subscription is to call the cleanupDeps() method. For example, in the template, v-if we have collected the dependencies in the qualified template A. when the conditions change, template b is displayed and template a is hidden. In this case, we need to remove the dependencies of A
The main tasks here are:
- First, traverse the last added instance array deps, and remove the Watcher subscription in the dep.subs array
- Then exchange newDepIds and depIds, and exchange newDeps and deps
- Then clear newDepIds and newDeps
// Cleaning up unwanted dependencies cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } 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 }
Distribute updates
notify()
When the setter is triggered, dep.notify() will be called to notify all subscribers to send updates
notify () { const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // If it is not asynchronous, sorting is required to ensure that it is triggered correctly subs.sort((a, b) => a.id - b.id) } // Traverse all watcher instance arrays for (let i = 0, l = subs.length; i < l; i++) { // Trigger update subs[i].update() } }
update()
Called when an update is triggered
update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { // Component data update will go here queueWatcher(this) } }
queueWatcher()
Source address: Src / core / observer / scheduler.js - line 164
This is a queue and an optimization point for Vue when distributing updates. That is, the watcher callback will not be triggered every time the data changes, but these watchers will be added to a queue and executed after nextTick
The logic here overlaps with that of flushscheduler queue () in the next section, so it should be understood together
The main tasks are:
- First use the has object to find the id to ensure that the same watcher will only push once
- else, if a new watcher is inserted during the execution of the watcher, it will come here. Then, look back and forward, find the position where the first id to be inserted is larger than the id in the current queue, and insert it into the queue. In this way, the length of the queue will change
- Finally, ensure that nextTick will only be called once through waiting
export function queueWatcher (watcher: Watcher) { // Get the id of the watcher const id = watcher.id // Judge whether the watcher of the current id has been push ed if (has[id] == null) { has[id] = true if (!flushing) { // I'll be here at first queue.push(watcher) } else { // When executing the following flushscheduler queue, if there is a newly distributed update, it will enter here and insert a new watcher, as described below let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // I'll be here at first if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // Because each update will cause rendering, all watcher s are called in nextTick nextTick(flushSchedulerQueue) } } }
flushSchedulerQueue()
Source address: Src / core / observer / scheduler.js - line 71
The main tasks here are:
- First sort the queue. There are three sorting conditions. See the notes
- Then traverse the queue and execute the corresponding watcher.run(). It should be noted that the queue length will be evaluated every time during traversal, because after run, a new watcher may be added, and the above queueWatcher will be executed again
function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // Sort by id, with the following conditions // 1. Component updates need to be in the order from parent to child, because the creation process also starts from parent to child // 2. The watcher written by ourselves in the component takes precedence over the render watcher // 3. If a component is destroyed during the operation of the watcher of the parent component, skip the watcher queue.sort((a, b) => a.id - b.id) // Do not cache the queue length, because the queue length may change during traversal for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { // Execute beforeUpdate lifecycle hook function watcher.before() } id = watcher.id has[id] = null // Execute the callback function of watch written by ourselves in the component and render the component watcher.run() // Check and stop the cyclic update. For example, if the object is re assigned during the watcher process, it will enter the infinite loop if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn(`Infinite cycle`) break } } } // Keep a backup of the queue before resetting the status const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // Call the hook activated by the component callActivatedHooks(activatedQueue) // Call the hook updated for component update callUpdatedHooks(updatedQueue) }
updated()
Finally, it can be updated. Updated is familiar to everyone. It is the life cycle hook function
When callUpdatedHooks() is called above, it will enter here and execute updated
function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, 'updated') } } }
So far, the source code of Vue2's responsive principle process has been basically analyzed. Next, let's introduce the shortcomings of the above process
defineProperty defects and handling
There are still some problems with using Object.defineProperty to implement responsive objects
- For example, when adding a new property to an object, the setter cannot be triggered
- For example, changes in array elements cannot be detected
Vue2 also has corresponding solutions to these problems
Vue.set()
When adding new responsive properties to an object, you can use a global API, the Vue.set() method
Source address: Src / core / observer / index.js - line 201
The set method receives three parameters:
- target: array or ordinary object
- Key: indicates the array subscript or the key name of the object
- val: indicates the new value to replace
The main tasks here are:
- First, if it is an array and the subscript is legal, replace it directly with the rewritten splice
- If it is an object and the key exists in the target, replace the value
- If not__ ob__, Description is not a responsive object. It is returned by direct assignment
- Finally, the new attribute is changed into a response type and the update is distributed
export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // If it is an array and is a legal subscript if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // Use the splice directly to replace it. Note that the splice here is not native, so it can be monitored. See the following for details target.splice(key, 1, val) return val } // This indicates that it is an object // If the key exists in the target, it can be directly assigned and monitored if (key in target && !(key in Object.prototype)) { target[key] = val return val } // Get target__ ob__ const ob = (target: any).__ob__ 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 } // As described in Observer, if there is no such attribute, it means that it is not a responsive object if (!ob) { target[key] = val return val } // Then change the newly added attribute into a response defineReactive(ob.value, key, val) // Manually distribute updates ob.dep.notify() return val }
Override array method
Source address: src/core/observer/array.js
The main tasks here are:
- Save a list of methods that will change the array
- When executing some methods in the list, such as push, first save the original push, then perform responsive processing, and then execute this method
// Gets the prototype of the array const arrayProto = Array.prototype // Create an object that inherits the array prototype export const arrayMethods = Object.create(arrayProto) // Will change the method list of the original array const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] // Override array events methodsToPatch.forEach(function (method) { // Save original event const original = arrayProto[method] // Create a responsive object 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) // Distribute updates ob.dep.notify() // After finishing the processing we need, we can execute the original event return result }) })
Previous highlights
- How does the render function come from? Template compilation in Vue
- Explain the virtual DOM and Diff algorithm in simple terms, and the differences between Vue2 and Vue3
- The 7 components of Vue3 communicate with the 12 components of Vue2, which is worth collecting
- What has been updated in the latest Vue3.2
- JavaScript advanced knowledge points
- Front end anomaly monitoring and disaster recovery
- 20 minutes to help you win HTTP and HTTPS and consolidate your HTTP knowledge system
epilogue
If this article is of little help to you, please give me a praise and support. Thank you