Welcome to star my github warehouse and learn together ~ at present, 5 pieces of vue source code learning series have been updated~
https://github.com/yisha0307/...
Quick jump:
- Vue's two-way binding principle (completed)
- Talk about Virtual DOM in vue (completed)
- The implementation difference between React diff and Vue diff
- Asynchronous update policy in Vue (completed)
- Implementation understanding of Vuex
- Typescript learn ing notes (continuously updated)
- Use of closures in Vue source code (completed)
introduce
Recently, I was learning the source code of Vue and vuex, and recorded some of my learning experiences. It mainly draws lessons from the Vue.js source code analysis by ran Mo and the mvvm of DMQ. The former annotated the source code of Vue and vuex and detailed Chinese doc. The latter analyzed the implementation principle of Vue and manually implemented mvvm.
My records are mainly about my understanding of vue source code. If you want to see a more systematic introduction, I still recommend the projects of the two great gods listed above~
The English notes of the code quoted in the article are especially large, some of the Chinese notes are dyed unfamiliar, and some are my supplementary understanding. There may be some misunderstandings. You are welcome to comment or mention the issue to my github. Thank you~
Data binding principle
As Youda wrote in Vue's tutorial in-depth response principle, Vue mainly relies on hijacking object The getter and setter methods of defineproperty () inject dependencies every time you get data, and publish messages to subscribers when you set data, so as to update the DOM. Because object Defineproperty () cannot be implemented in IE8 or earlier browsers, so Vue cannot support these browsers.
Take a look at the pictures provided on the official website:
During the component rendering operation ("Touch"), trigger the getter in the data (it will ensure that only the data used will trigger the dependency. After looking at the source code, in fact, the value corresponding to the key of each data will have a Dep to collect a group of subs < array: Watcher >). When updating the data, trigger the setter and notify all subs to update through Dep.notify(), The watcher notifies the component to update through the callback function, optimize s through virtual DOM and diff calculation, and completes re render.
In the whole source code, several important concepts are Observer/Dep/Watcher. The following mainly analyzes the processing of these three types.
Observer
The function of observer is to listen to the whole data. Observe (data) is used in the initial method initData. The getter and setter of each attribute of data are hijacked by the defineReactive method inside the Observer class. Take a look at the source code:
export function observe (value: any, asRootData: ?boolean): Observer | void { /*Determine whether Data is an object*/ if (!isObject(value)) { return } let ob: Observer | void /*Used here__ ob__ This attribute is used to determine whether there is an Observer instance. If there is no Observer instance, a new Observer instance will be created and assigned to__ ob__ This property. If there is an Observer instance, the Observer instance will be returned directly*/ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( /*The judgment here is to ensure that value is a simple object, not a function or Regexp.*/ observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // Create an Observer instance and bind data to listen ob = new Observer(value) } if (asRootData && ob) { /*If it is root data, count. The asRootData of observe in the following Observer is not true*/ ob.vmCount++ } return ob }
Vue's responsive data will have one__ ob__ As a tag, the Observer instance is stored in it to prevent repeated binding.
Take another look at the source code of the Observer class:
/** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */ export class { value: any; dep: Dep; // Each Data attribute will be bound with a dep to store the watcher arr vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 /* /vue-src/core/util/lang.js: function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } */ def(value, '__ob__', this) // This def means to bind the Observer instance to the Data instance__ ob__ Attribute up if (Array.isArray(value)) { /* If it is an array, replace the original method in the prototype of the array with the modified array method that can intercept the response, so as to achieve the effect of listening to the response of array data changes. */ const augment = hasProto ? protoAugment /*Modify the target object by directly overriding the prototype*/ : copyAugment /*Defines (overrides) a method of the target object or array*/ augment(value, arrayMethods, arrayKeys) /*Github:https://github.com/answershuto*/ /*If it is an array, you need to traverse each member of the array for observe*/ this.observeArray(value) } else { /*If it is an object, bind it directly to walk*/ this.walk(value) } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) /* walk Method will traverse each attribute of the object for defineReactive binding defineReactive: Hijack the getter and setter of data */ for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { /* The array needs to traverse each member for observe */ for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
You can see that the Observer class mainly does the following things:
- Bind a to data__ ob__ Property to store the Observer instance to avoid repeated binding
- If the data is an Object, traverse each attribute of the Object for defineReactive binding
- If the data is Array, you need to observe each member. vue.js will override the seven methods of Array push, pop, shift, unshift, splice, sort and reverse to ensure that the objects that are operated by pop/push are also bound in both directions (see observer / Array. JS for specific code)
defineReactive()
As shown in the above source code, the Observer class mainly uses the defineReactive() method to hijack the getter and setter methods by traversing each attribute of data. Let's take a look at defineReactive:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: Function ) { /*Define a dep object in the closure*/ const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } /*If the getter and setter functions have been preset for the object before, they will be taken out and executed in the newly defined getter/setter to ensure that the previously defined getter/setter will not be overwritten.*/ // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set /*Object's children are also observe d*/ let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { /*If the original object has a getter method, execute*/ const value = getter ? getter.call(obj) : val // Dep.target: global attribute. It is used to point to a certain watcher and lose it when used up if (Dep.target) { /* Perform dependency collection dep.depend()Internally implement addDep and add a watcher instance to dep (refer to dep.prototype.dependent code for details) depend It will judge whether the watcher has been added according to the id to avoid adding dependencies repeatedly */ dep.depend() if (childOb) { /*The dependency collection of sub objects is actually to put the same watcher observer instance into two dependencies, one is the dependency in its own closure, and the other is the dependency of sub elements*/ childOb.dep.depend() } if (Array.isArray(value)) { /*If it is an array, you need to collect dependencies on each member. If the member of the array is still an array, it is recursive.*/ dependArray(value) } } return value }, set: function reactiveSetter (newVal) { /*Get the current value through the getter method and compare it with the new value. If it is consistent, you do not need to perform the following operations*/ 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() } if (setter) { /*If the native object has a setter method, the setter is executed*/ setter.call(obj, newVal) } else { val = newVal } /*The new value needs to be observe d again to ensure data responsiveness*/ childOb = observe(newVal) /*dep Object notifies all observers*/ dep.notify() } }) }
The defineReactive() method mainly uses object Defineproperty () does the following:
- Define a Dep instance in the closure;
- getter is used to collect dependencies. Dep.target is a global attribute. The watcher pointed to is collected in dep (if it has been added before, it will not be added repeatedly);
- setter notifies all getter s when updating value, and notifies all collected dependencies to update (dep.notify). A judgment will be made here. If newVal is the same as oldVal, there will be no operation.
Dep
Dep is mentioned in defineReactive above, so let's take a look at the source code of dep. Dep is mainly used to notify watchers to update when data is updated:
/** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ // subs: Array<Watcher> this.subs = [] } /*Add an observer object*/ addSub (sub: Watcher) { this.subs.push(sub) } /*Remove an observer object*/ removeSub (sub: Watcher) { remove(this.subs, sub) } /*Depending on the collection, add the observer object when the Dep.target exists*/ // dep.depend() is used in the getter of defineReactive depend () { if (Dep.target) { // Dep.target points to a watcher Dep.target.addDep(this) } } /*Notify all subscribers*/ notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { // Call the update of each watcher subs[i].update() } } } // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null /*After dependency collection, set Dep.target to null to prevent repeated dependency addition later.*/
- Dep is a publisher that can subs cribe to multiple observers. After dependency collection, a sub in dep will store one or more observers and notify all watcher s when data changes.
- Again, the relationship between Dep and Observer is that Observer listens to the whole data, traverses each attribute of data, binds defineReactive methods to each attribute, hijacks getters and setters, inserts dependencies (dep.depend) into Dep class when getters, and notifies all watcher s to update(dep.notify) when setters
Watcher
After receiving the notification, the watcher will update through the callback function.
Next, we'll take a closer look at the source code of the watcher. It can be seen from the previous Dep code that the watcher needs to achieve the following two functions:
- Add yourself to dep when dep.depend();
- Call watcher. When dep.notify() Update () method to update the view;
It should also be noted that there are three types of watchers: render watcher / computed watcher / user watcher (that is, the watch in the vue method)
export default class Watcher { vm: Component; expression: string; // string corresponding to each DOM attr cb: Function; // Callback function during update id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: ISet; newDepIds: ISet; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm /*_watchers Store subscriber instances*/ vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter /*Parse the expression expOrFn into a getter*/ if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ /*Get the value of getter and start dependency collection again*/ get () { /*Set the self watcher observer instance to Dep.target to rely on the collection.*/ pushTarget(this) let value const vm = this.vm /* After the getter operation, it seems that the rendering operation is performed, but in fact the dependency collection is performed. After Dep.target is set as its own observer instance, execute the getter operation. For example, there may be a, b and c data in the current data. getter rendering needs to rely on a and c, Then the getter functions of a and c data will be triggered when the getter is executed, In the getter function, you can judge whether Dep.target exists, and then complete dependency collection, Put the observer object into the sub of Dep in the closure. */ if (this.user) { // this.user: judge whether it is the watcher bound by the watch method in vue try { value = this.getter.call(vm, vm) } catch (e) { handleError(e, vm, `getter for watcher "${this.expression}"`) } } else { value = this.getter.call(vm, vm) } // "touch" every property so they are all tracked as // dependencies for deep watching /*If deep exists, the dependency of each deep object is triggered to track its change*/ if (this.deep) { /*Recurse each object or array and trigger their getter s, so that each member of the object or array is collected to form a "deep" dependency*/ traverse(value) } /*Take the observer instance from the target stack and set it to Dep.target*/ popTarget() this.cleanupDeps() return value } /** * Add a dependency to this directive. */ /*Add a dependency to the Deps collection*/ // In dep.depend(), Dep.target. is called. addDep() addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { // newDepIds and newDeps record the DEPs used by the watcher instance. For example, if a calculated watcher is applied to the three attributes a/b/c in data, three DEPs need to be recorded this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // The function is to add yourself (Watcher instance) to the sub of dep // However, we will judge the id first. If there is the same id in the subs, it will not be added repeatedly dep.addSub(this) } } } /** * Clean up for dependency collection. */ /*Clean up dependency collection*/ cleanupDeps () { /*Remove all observer objects*/ 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 } /** * Subscriber interface. * Will be called when a dependency changes. */ // During dep.notify, the update method of the watcher will be called one by one update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*In synchronization, run is executed to render the view directly*/ // sync is rarely used this.run() } else { /*Asynchronously pushed to the observer queue and invoked by the scheduler.*/ queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ /* The dispatcher work interface will be called back by the dispatcher. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. /* Even if the values are the same, observers with the Deep attribute and observers on the object / array should be triggered to update because their values may change. */ isObject(value) || this.deep ) { // set new value const oldValue = this.value /*Set new value*/ this.value = value /*Trigger callback render view*/ 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) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ /*Gets the value of the observer*/ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ /*Collect all deps dependencies of the watcher*/ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ /*Removes itself from the list of all dependent collection subscriptions*/ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. /*Remove itself from the observer list of the vm instance. Since this operation is resource-consuming, skip this step if the vm instance is being destroyed.*/ if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
It should be noted that there is a sync attribute in the watcher. In most cases, the watcher is not updated synchronously, but is updated asynchronously, that is, call queueWatcher(this) to push it to the observer queue and call it when nextTick.
Reproduced at: Interpretation of vue source code (I) how Observer/Dep/Watcher implements data binding - moon3 - blog Garden