preliminary

Recently a period of time in reading Vue source code, from its core principle, began to learn the source code, and its core principle is the response of its data, when it comes to the response principle of Vue, we can start from its compatibility, Vue does not support IE8 following version of the browser, Object. DefineProperty is a feature in ES5 that cannot be shim. This is why Vue does not support IE8 and later browsers. Vue updates view data by listening for collected dependencies through getters/setters of Object.defineProperty, notifying changes when properties are accessed and modified;

Restricted (and obsolete) by modern JavaScriptObject.observeVue cannot detect the addition or deletion of object attributes. Since Vue executes on properties when instantiatinggetter/setterThe transformation process, so the property must be indataObject exists in order for Vue to convert it so that it is responsive.



Here we are based on Vue2.3 source code for analysis,Vue data responsive changes mainly involve Observer, Watcher, Dep these three main classes; So understanding Vue responsiveness requires understanding how these three classes are related to each other; And how they work, responsible for the logical operations. So let’s analyze the reactive principle of Vue from the code of a simple Vue instance

var vue = new Vue({
    el: "#app",
    data: {
        name: 'Junga'
    },
    created () {
        this.helloWorld()
    },
    methods: {
        helloWorld: function() {
            console.log('my name is' + this.name)
        }
    }
    ...
})Copy the code

Vue initializes an instance

As we know from the Vue lifecycle, the Vue first performs init initialization; The source code in the SRC/core/instance/init. Js

/* Initialize lifecycle */ initLifecycle(VM) /* Initialize events */ initEvents(VM) object.defineProperty /* Initialize render*/ initRender(vm) /* Call the beforeCreate hook function and trigger the beforeCreate hook event. 'beforeCreate') initInjections(VM) // resolve injections before data/props /* Initialize props, methods, data, computed, and watch*/ InitState (VM) initProvide(VM) // resolve provide after data/props /* Call created hook functions and trigger created hook events */ callHook(vm, 'created')Copy the code

The above code can see initState (vm) is used to initialize the props, the methods, data, computed and watch;

src/core/instance/state.js

/* Initialize props, methods, data, computed, and watch*/ export function initState (VM: Component) {vm._watchers = [] const opts = vm.$options /* initialize props*/ if (opts.props) initProps(vm, Opts.props) /* Initialization method */ if (opts.methods) initMethods(vm, Opts.methods) /* Initialize data*/ if (opts.data) {initData(vm)} else {/* Bind an empty object when there is no data*/ observe(vm._data = {}, True /* asRootData */)} /* Initialize computed*/ if (opts.computed) initComputed(VM, Opts.com puted) /* Initialize Watchers */ if (opts.watch) initWatch(vm, opts.watch)}... /* Initialize data*/ function initData (vm: $options. Data data = vm._data = typeof data === 'function'? getData(data, vm) : data || {}defi ... While (I --) {/* The key of data is not the same as the key of props. Warning */ if (props && hasOwn(props, keys[I])) {process.env.node_env! == 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (! If (keys[I]) {/* if (keys[I]) {/* if (keys[I]) { */ proxy(vm, '_data', keys[I])}} // observe data /* AsRootData is the root data, which is used to calculate the number of root data instantiated. In the following section, recursive observe is performed to bind the deep object. AsRootData not true*/ observe(data, true /* asRootData */)}Copy the code

1, the initData

Observe (data, true/asRootData /); observe(data, true/asRootData /); That is, getter/setter operations are performed on each property defined by Data. This is the basis for Vue’s responsive implementation; Observe the implementation of the following SRC/core/observer/index, js

/* Attempt to create an Observer instance (__ob__) and return a new Observer instance if it is successfully created or an existing Observer instance if it already exists. */ export function observe (value: any, asRootData: ? boolean): Observer | void { if (! isObject(value)) { return } let ob: The Observer | void / * here have been use __ob__ this property to determine whether the Observer instance, if there is no Observer instance will create a new Observer instance and assigned to __ob__ this property, If an Observer instance already exists, return it directly. Def (value, '__ob__', this)*/ if (hasOwn(value, '__ob__') &&value.__ob__ instanceof Observer) {ob = value.__ob__} else if ( Not functions or regexps or whatever. And the object should be observed only when shouldConvert is applied. This is a logo, avoid repetition of the value of the Observer. * / observerState shouldConvert &&! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && ! Value._isvue) {ob = new Observer(value)} if (asRootData && ob) { Behind the Observer of observe asRootData of true * / ob vmCount++} return ob}Copy the code

The new Observer(value) method is one of the core methods to implement responsiveness. It is used to convert data to be observed. Use Watcher to watch the data change and then update it into the view.

2, the Observer

The Observer class converts the key value of each target object (that is, data) into getter/setter form for dependency collection and scheduling updates.

src/core/observer/index.js

export class Observer { value: any; dep: Dep; 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 /* Bind the Observer instance to the __ob__ attribute of data, Observe has an __ob__ object that stores an Observer instance. / SRC /core/util/lang.js*/ def(value, '__ob__', this) if (array.isarray (value)) { The modified array method that can intercept the response is replaced by the original method in the prototype of the array, so as to achieve the effect of listening to the response of the array data changes. Here, if the current browser supports the __proto__ attribute, the native array method on the current array object prototype is overridden directly, if not, the array object prototype is overridden directly. */ const augment = hasProto ? ProtoAugment /* Directly overlays the prototype method to modify the target object */ : CopyAugment /* Defines (overlays) a method of the target object or array */ augment(value, arrayMethods, Observe */ this.observeArray(value)} else {/* walk */ this.walk(value)} Walk (obj: Object) {const keys = object.keys (obj) /* Walk (let I = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } }Copy the code
  1. First bind the Observer instance to the OB property of data to prevent repeated binding.
  2. If data is an array, first implement the corresponding variation method (the variation method means that Vue has rewritten 7 native methods of the array, which will be explained later), and then observe each member of the array to make it a responsive data.
  3. Define defineReative(obj, keys[I], obj[keys[I]]). Define defineReative(obj, keys[I], obj[keys[I]])
export function defineReactive ( obj: Object, key: string, val: any, customSetter? : Function) {/ * * a dep Object defined in the closure/const dep = new dep () const property = Object. GetOwnPropertyDescriptor (obj, Key) if (property && property.64x === false) {return} /* This works without preset getter and setter functions. This is executed in the newly defined getter/setter, ensuring that it does not overwrite the previously defined getter/setter. */ // cater for pre-defined getter/setters const getter = property && property.get const setter = property && Property. Set /* The child of the Object recursively observes and returns the Observer of the child. */ let childOb = observe(val) object.defineProperty (obj, key, { enumerable: true, configurable: true, get: Function reactiveGetter () {/* If the original object has a getter method, call */ const value = getter? Getter. call(obj) : Val if (dep.target) {/* To collect dependencies */ dep.depend() if (childOb) {/* To collect dependencies, put the same watcher instance into two depend instances. If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)) {/* If (array.isarray (value)); The recursion. */ dependArray(value) } } return value }, set: Function reactiveSetter (newVal) {/* Get the current value from the getter method and compare it with the 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()} if (setter) {/* If the original object has setter methods, execute setter*/ setter.call(obj, NewVal)} else {val = newVal} /* */ childOb = observe(newVal) /*dep notify all observers */ dep.notify()}})}Copy the code

Where the getter method:

  1. A Dep instance object is declared for each data, which is used in getters to collect associated dependencies by executing dep.depend().
  2. Determine whether to collect dependencies based on dep. target or a common value. When and how the dep. target is collected will be explained later.

So why do we collect dependencies?

new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
            <span>text2:</span> {{text2}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});Copy the code

We can see from the above code that Text3 in data is not actually used by the template. In order to improve the efficiency of code execution, we do not need to respond to it. Therefore, the simple understanding of dependency collection is to collect data only used in the actual page, and then mark it, in this case, marked as DEP.target.

In setter methods:

  1. Obtain new values and observe to ensure data responsiveness;
  2. The deP object informs all observers to update the data in a responsive manner.

In the Observer class, we can see that in the getter, the DEP collects the dependency, that is, the watcher of the dependency, and then notifits Watcher of the change through the DEP during the setter operation, and watcher performs the change. We use a diagram to describe the relationship between the three:

From the picture, we can understand simply: Dep can be regarded as a bookstore, Watcher is the bookstore subscriber, and Observer is the book of the bookstore. Subscribers can add subscriber information when subscribing to books in the bookstore, and once there is a new book, messages will be sent to subscribers through the bookstore.

3, Watcher

Watcher is an observer object. After the dependency collection, the Watcher object is stored in the SUBs of the Dep. The Dep notifies the Watcher instance when the data changes, and the Watcher instance calls cb to update the view.

src/core/observer/watcher.js

export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? */ 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 expOrFn /* expOrFn = url */ 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 () {/* Set your watcher instance to dep.target. */ pushTarget(this) let value const vm = this.vm /* After setting dep. target to the spontaneous observer instance, perform the getter operation. For example, there may be a, B, and C data in the current data, and the getter rendering needs to rely on A and C, so the getter function of a and C data will be triggered when the getter is executed. In the getter function, the existence of dep. target can be determined and the dependency collection can be completed. Put the observer object into the subs of the Dep in the closure. */ if (this.user) { 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 Trace its changes */ if (this.deep) {/* Recurse to each object or array, triggering their getter so that each member of the object or array is dependently collected, Create a "deep" dependency */ traverse(value)} /* Pull observer instance from target stack and set to dep.target */ popTarget() this.cleanupdeps () return value} /** * Add a dependency to this directive. */ 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)) {dep.addSub(this)}}} /** * Clean up for dependency collection. /* Remove all observer objects */... } /** * Subscriber interface. * Will be called when a dependency changes. */ /* Scheduler interface to call back when a dependency changes. */ Istanbul ignore () {/* Istanbul ignore else */ if (this.lazy) {this.dirty = true} else if (this.sync) {/* run to render view */ This.run ()} else {/* Asynchronously pushed to observer queue, called on next tick. */ queueWatcher(this)}} /** ** Scheduler job interface. * Will be called by the Scheduler. */ run () {if (this.active) {/* get gets the value itself and executes the getter to call the update view */ 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 the new value const oldValue = this. The value to set new values * / / * this. Value = the value / * trigger callback * / 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. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ /* Collect all deps dependencies for the watcher */ depend() {let I = this.deps. Length while (I --) {this.deps[I].depend()}} /** * Remove self From all dependencies' subscriber list. */ /* Delete subscriber list */ teardown () {... }}Copy the code

4, Dep

When the getter is triggered by the Observer’s data, the Dep will collect the dependent Watcher. In fact, the Dep is a bookstore that can accept multiple subscribers. When a new book is available, that is, when the data changes, Watcher will be notified of the update via Dep.

src/core/observer/dep.js

export default class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>; Constructor () {this.id = UI ++ this.subs = []} /* Add an observer object */ addSub (sub: Watcher) {this.subs.push(sub)} /* Remove an observer object */ removeSub (sub: Watcher) {remove(this.subs, sub)} Stabilize add observer object */ Depend () {if (dep.target) {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++) { subs[i].update() } } }Copy the code

conclusion

In fact inVueWhen the render is initialized, the data bound to the view is instantiatedWatcherDependency collection is done by attributesgetterFunction is done, which is what we started this article withObserverWatcherDepAll related to dependency collection. Among themObserverDepIt’s a one-to-one relationship,DepWatcherIt’s many-to-many,DepIt isObserverWatcherThe bond between. After dependency collection is complete, property changes are executed byObserverThe object’sdep.notify()Method, which iterates through the Watcher list to send a message,WatcherWill performrunMethod to update the view, let’s take a look at another diagram to summarize:

  1. In Vue, instructions or data binding during template compilation instantiate an instance of Watcher, which triggers get() to point itself to dep.target.
  2. Getter execution of data in an Observer triggers dep.depend() for dependency collection; Data adds Watcher instance to subs of deP instance of closure in Observer; 2. Add the closure DEP of the Observer to the DEPS of Watcher;
  3. When the value of an object in the data is changed by an Observer, the watcher observing it in subs executes the update() method, which actually calls the watcher callback cb to update the view.

reference

  • Vue source
  • Vue document
  • Vue source code learning