This post was originally posted on github Blog.

This article is analyzed according to Vue source code V2.x. Here only comb the most important part of the source code, skip some non-core parts. Reactive updates mainly involve Watcher, Dep, and Observer classes.

This article is mainly to clarify the following problems that are easy to confuse:

  • Watcher.Dep.ObserverWhat is the relationship between these classes?
  • DepIn thesubsWhat is stored?
  • WatcherIn thedepsWhat is stored?
  • Dep.targetWhat is it? Where is the value assigned?

This article directly starts from creating a Vue instance, step by step to uncover the responsive principle of Vue, assuming the following simple Vue code:

var vue = new Vue({
    el: "#app".data: {
        counter: 1
    },
    watch: {
        counter: function(val, oldVal) {
            console.log('counter changed... ')}}})Copy the code

1. Initialize the Vue instance

From the life cycle of the Vue, init initialization is performed first, which is in instance/init.js.

src/core/instance/init.js

initLifecycle(vm) // Initialize the vm lifecycle related variables
initEvents(vm) // VM event-related initialization
initRender(vm) // Template parsing related initialization
callHook(vm, 'beforeCreate') // Call the beforeCreate hook function
initInjections(vm) // resolve injections before data/props 
initState(vm) // Initialize the vm state.
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // Call the created hook function
Copy the code

InitState (VM), which implements initialization operations for props, methods, data, computed, and watch, is the focus of the study. Here, based on the above examples, we focus on data and Watch. The source code is located in instance/state.js

src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // Initialize the VM's data, mainly by setting the corresponding getter/setter methods through the Observer
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  // Initialize the added watch
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

2. initData

A Vue instance implements getter/setter methods for each of its data, which is the basis for a responsive implementation. See MDN Web docs for getters/setters. Counter = this.counter = this.counter = this.counter = this.counter When changing the value this.counter = 10, you can also customize some actions when setting the value. The implementation of initData(VM) is instance/state.js in the source code.

src/core/instance/state.js

while (i--) {
	...
    // here we want to delegate all the data on the data, props, and methods to the vue instance
	// make vm.counter accessible directly
}
// Skip the previous code and go straight to the core observe method
// observe data
observe(data, true /* asRootData */)

Copy the code

Here the observe() method makes data observable. Why is it observable? The main thing is to implement getter/setter methods so that Watcher can observe changes to the data. Let’s look at the implementation of Observe.

src/core/observer/index.js

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( observerState.shouldConvert && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value) // This is where the core of responsiveness lies
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Here we focus only on the New Observer(Value), which is the heart of the method, using the Observer class to make vue’s data responsive. In our case, the value of the input parameter is {counter: 1}. Let’s look at the Observer class in detail.

3. Observer

First look at the constructor of this class, which is first implemented by the New Observer(Value). The author’s comments state that the Observer Class converts the key value of each target object (that is, the data in data) into getter/setter form for dependency collection and update via dependency notification.

src/core/observer/index.js

/** * 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 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
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value) // Iterate over the data object {counter: 1,.. } for each key value (such as counter), set its setter/getter methods.}}... }Copy the code

This. Walk (value) ¶ This. ObserveArray (value) ¶

Moving on to the walk() method, it is stated in the comments that all walk() does is walk through the data for each set in the data object, converting it into a setter/getter.

  /** * 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)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
Copy the code

The defineReactive() method is the final way to convert the corresponding data into a getter/setter. It is also easy to know from the method name that the method is defined to be responsive. Combining with the original example, the call here is defineReactive(…). As shown in the figure:

The source code is as follows:

export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
  // dep is a dependent instance of the current data
  // DeP maintains a list of subs that hold observers (or subscribers) that depend on the current data (in this case, the current data is counter). The observer is the Watcher instance.
  const dep = new Dep() ---------------(1)

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  letchildOb = ! shallow && observe(val)// Define getter and setter
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      // Dependency collection is done before fetching the value, if dep.target has a value.
      if (Dep.target) {    -----------------(2)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      
      // Depends on the value returned after collection
      return value
    },
    
    ...
}
Copy the code

Let’s start with the getter method, which has two important things.

  1. Declare one for each datadepInstance object, followed bydepIs referenced to the closure by the corresponding data. For example, every timecounterIts DEP instance is accessible when it is set or modified and does not disappear.
  2. According to theDep.targetTo determine whether to collect dependencies, or a common value. hereDep.targetAnd we’ll do that later, but we know that this is the case.

Then look at the setter method, source code is as follows:

set: function reactiveSetter (newVal) {
  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()
  }
  // Change the value of the data
  if (setter) {
    setter.call(obj, newVal)
  } else{ val = newVal } childOb = ! shallow && observe(newVal)// The most important step is to notify the observer through the DEP instance that my data is updated
  dep.notify()
}
Copy the code

Initialization of Vue instance data is now complete. The following is a review of initData:

The next step is to initialize the watch:

src/core/instance/state.js

export function initState (vm: Component) {... if (opts.data) { initData(vm)// Initialize the VM's data, mainly by setting the corresponding getter/setter methods through the Observer
  } 
  
  // initData(VM) initWatch(..).// Initialize the added watch
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

4. initWatch

Here initWatch(VM, opts.watch) corresponds to our example as follows:

InitWatch:

src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    // Handler is a callback function to the observed object
    // As in the example counter callback function
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code

CreateWatcher (vm, key, handler) createWatcher(VM, key, handler)

function createWatcher (vm: Component, keyOrFn: string | Function, handler: any, options? : Object) {
  // Check if it is an object. If it is, fetch the handler method inside the object
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // Check whether handler is a string, if it is a method on the VM instance
  // Get this method from vm[handler]
  // If handler='sayHello', then handler= vm.sayHello
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  
  // Finally call $watch(...) on the VM prototype chain. The Watcher () method creates a Watcher instance
  return vm.$watch(keyOrFn, handler, options)
}
Copy the code

$watch is a method defined on the Vue prototype chain.

core/instance/state.js

  Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // Create a Watcher instance object
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    
    // This method returns a reference to a function that calls the Teardown () method of the Watcher object to remove itself from its registered list (subs).
    return function unwatchFn () {
      watcher.teardown()
    }
  }
Copy the code

After a lot of wrapping, you finally see the create Watcher instance object. The Watcher class is explained in detail below.

5. Watcher

According to our example, new Watcher(…) As shown below:

First, execute the Watcher class constructor, source code as follows, omits some code:

core/observer/watcher.js

  constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) { ... this.cb = cb// Save the callback function passed in
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = [] // Save the observed data to the current DEP instance object
    this.newDeps = []  // The latest DEP instance object to save observed data
    this.depIds = new Set(a)this.newDepIds = new Set(a)// parse expression for getter
    // Get the get method of the observed object
    // For calculating attributes, expOrFn is a function
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
    // Obtain the expOrFn get method of observed objects using the parsePath method
      this.getter = parsePath(expOrFn)
      ...
    }
    
    // Finally, by calling the watcher instance get() method,
    // This method is the key to associating watcher instances with observed objects
    this.value = this.lazy
      ? undefined
      : this.get()
  }
Copy the code

The specific implementation methods of parsePath(expOrFn) are as follows:

core/util/lang.js

/** * Parse simple path. */
const bailRE = /[^\w.$]/ // Matches any string that does not match any combination of words and numbers containing underscores
export function parsePath (path: string) :any {
  // Invalid string returns directly
  if (bailRE.test(path)) {
    return
  }
  Split ('.') --> ['counter']
  const segments = path.split('. ')
  // Return a function to this.getter
  // So this.getter.call(vm, vm), where vm is the input parameter to the return function obj
  // It actually calls the vm instance data, such as vm.counter, which triggers the getter for counter.
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

This neatly returns a method to this.getter, that is:

this.getter = function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
}
Copy the code

Getter is called inside the this.get() method to get the value of the watch object and trigger its dependency collection, in this case counter.

The last step in the Watcher constructor method is to call this.get().

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    // This method actually sets dep. target = this
    // Set dep. target to the Watcher instance
    // Dep.target is a global variable that can be used once the getter method in the observation data is set
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Call the getter method to observe the data
      // Make dependencies to collect and obtain values for observed data
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // At this point the observation data dependencies have been collected
      / / reset Dep. Target = null
      popTarget()
      // Remove the old deps
      this.cleanupDeps()
    }
    return value
  }
Copy the code

The key steps have been commented in the above code. Here is an example of the relationship between Observer and Watcher classes:

  • Red arrow: Instantiate the Watcher class, calling the Watcher instanceget()Method and setDep.targetIs the current watcher instance that triggers the watch objectgetterMethods.
  • Blue arrow:counterThe object’sgetterMethod is raised and calleddep.depend()Do dependency collection and returncounterThe value of the. Depending on the results of the collection:1.counterDep instance of the closuresubsAdd watcher instance W1 to watch it;2. The w1depsTo add the observed objectcounterThe closure of the dep.
  • Orange arrow: WhencounterThe value changes after the triggersubsTo observe its W1 executionupdate()Method, which actually ends up calling w1’s callback function cb.

Other related methods in the Watcher class are more intuitive and will be skipped here. Please refer to the Watcher class source code for details.

6. Dep

The Dep is associated with the Observer and Watcher classes. What is Dep?

Dep is a publishing house, Watcher is a reader, and Observer is a higashino keigo book. Such as reader w1 white night line of tolo keigo interested in (counter) in our example, readers w1 once bought tolo keigo book, then it will automatically in the press of the book instances (Dep) registered fill w1 inside information, once the press had tolo keigo this book the latest news will inform w1 (such as a discount).

Now look at the Dep source code:

core/observer/dep.js

export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  constructor () {
    this.id = uid++
    // Saves an array of watcher instances
    this.subs = []
  }

  // Add an observer
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // Remove the observer
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // Do dependency collection
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Notify the observer that the data has changed
  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

The Dep class is relatively simple, and the corresponding method is very intuitive. The most important thing here is to maintain an array subs that stores the observer instance Watcher.

7. To summarize

At this point, the three main classes have been studied, and you are now ready to answer the first few questions of this article.

Q1: What is the relationship between Watcher, Dep and Observer?

A1: Watcher is for the Observer to observe the data encapsulated by the Observer. Dep is the link between Watcher and observation data, and mainly plays a role of relying on collection and notification of update.

Q2: What are subs stored in the Dep?

A2: Subs stores the Watcher instance of the observer.

Q3: What does DEPS store in Watcher?

A3: DEPS stores the DEP instance in the observation data closure.

Q4: What is dep. target and where is the value assigned?

Target is a global variable that holds the current watcher instance and is assigned to the current watcher instance when new Watcher() is created.

8. Extension

Here’s an example of calculating a property:

var vue = new Vue({
    el: "#app".data: {
        counter: 1
    },
    computed: {
        result: function() {
            return 'The result is :' + this.counter + 1; }}})Copy the code

The value of result here is dependent on the value of counter, which can better reflect the responsive calculation of Vue. Computed properties are initialized using initComputed(VM, opts.computed), and if you follow the source code, you can see that there is also a Watcher instance created:

core/instance/state.js

  watchers[key] = new Watcher(
    vm,  // The current vUE instance
    getter || noop,  Function (){return 'The result is :' + this.counter + 1; }
    noop, // noop is defined as an empty method, where no callback function is replaced by noop
    computedWatcherOptions // { lazy: true }
  )
Copy the code

The schematic diagram is as follows:

The result property is evaluated here because it depends on this.counter, so set a watcher to look at the value of result. Then compute attributes are defined through definedComputed(VM, key, userDef). When result is computed, the getter for this.counter is fired, which makes the value of result depend on the value of this.counter.

Finally, the result calculation property is defined with its setter/getter property: Object.defineProperty(Target, Key, sharedPropertyDefinition). See the source code for more details.

9. The reference

  1. Vue official document
  2. Vue source
  3. Vue source code analysis