Before send stab the heart of Vue — detailed render function code, promised to explore the various parts of vue core process through the source code analysis, today to enter the second part of the responsive principle part of the source code analysis, promised a little late, please pat

First, analyze the working principle

Or before the routine, before reading the source code, first analysis principle



Deep into the responsivity principle
Object.defineProperty
render function code

Second, source code analysis

Remember an instance

Reading source code is a boring thing, with a question to find the answer, to be easier to read

<template>
...
</template>
<script>
  export default {
    data () {
      return {
        name: 'hello'
      }
    },
    computed: {
      cname: function () {
        return this.name + 'world'
      }
    }
  }
</script>
<style>
...
</style>Copy the code

To reduce interference, the example has been stripped down to just two key elements, the data attribute name, and the computational attribute cname that calls this attribute, where cname and name are the relationship between subscriber and subscriber. We now need to read the source code with such questions, how does cName become a subscriber of name, and when the name changes, how do we notify cName to update its data

Initialize data and inject observer capabilities

Reactive processing of source code in the SRC/core/observer directory, see meaning, it use the observer pattern, don’t try so hard to enter the directory first, at the time of Vue instance initialization, performs to the SRC/core/instance/state. The related state initialization logic in js, Take a look at this file:

export function initState (vm: Component) {
  ...
  if(opts.data) {// Initialize data initData(vm)}else {
    observe(vm._data = {}, true/* asRootData */)} // Initializes the calculated propertiesif (opts.computed) initComputed(vm, opts.computed)
  ...
}Copy the code

All of the initialization data and initialization calculation properties that we care about are executed here, so let’s look at initData first, and follow the method down, and see what initData ultimately does:

  // observe data
  observe(data, true /* asRootData */)Copy the code

The observe method is now available in the observer/index.js file, where you can find the observe method definition. Read along to find the next key signal:

ob = new Observer(value)Copy the code

This step creates an Observer instance from the data passed in, and then follows it to the Observer constructor to find, The constructor creates Observer objects recursively for each element of data (when data is an array) or for each attribute of data (when data is an object). The final method that works is defineReactive

export functiondefineReactive ( obj: Object, key: string, val: any, customSetter? : Function ) { const dep = new Dep() 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

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() { const value = getter ? Getter. call(obj) : val // The current Watcherif(dep.target) {// Add the current watcher to the subscriber sequence of this data dep.depend()if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else{val = newVal} childOb = observe(newVal) // Notify subscriber of data change dep.notify()}}Copy the code

Code posted a little bit more, but really not to make up the number of words, because here part of the omission may bring some confusion, I did not reduce, sorry. Object.defineproperty overwrites the set and get methods of the data. When the data is called, the set method adds the current Watcher dep. target (the current caller) to the subscriber sequence of the data. The set method sends a notification to all subscribers for recalculation. The DEP, defined in the Observer /dep.js file, defines the behavior of the data subscriber to subscribe, unsubscribe, etc., which is not posted here.

Recall that the name and the calculation property cname in our example, when the cname method executes, name is called, and its GET method is triggered, This is the watcher that the Cname corresponds to (a Watcher is created for each computed attribute when computed is initialized). When the name is changed and the set method is triggered, the watcher corresponding to the Cname is notified as the subscriber and the value of the Cname is recalculated

Initialize the calculation properties to create the Watcher

Back to SRC/core/instance/state. The calculation of js file attributes initComputed initialization logic, this method for calculating a career-best founded Watcher object attributes

  // create internal watcher for the computed property.
  watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)Copy the code

The observer/watcher. Js constructor calls this

this.value = this.lazy
      ? undefined
      : this.get()Copy the code

There are two steps in the this.get method that are particularly critical (for evaluating attributes, delayed evaluation is done, which is why the this.lazy flag is used) :

 get() {// Set the current caller to dep.target pushTarget(this)letValue const vm = this.vm try {// call method, calculate the value of the dependent party value = this.getter.call(vm, vm)} catch (e) {... } finally { ... }return value
  }Copy the code
  1. PushTarget (this) sets watcher to dep. target, which adds the current watcher to the subscriber sequence according to dep. target when the get method of the dependent data is called. The goal is to share dep.target when watcher depends on multiple data
  2. The this.getter.call(vm, vm) method is invoked to compute the value, for example, the getter for the property cname is its defining functionfunction(){this.name + 'world'}. At this point the dependent party’s GET method is triggered, and the whole process is strung together and makes sense

For computed properties, there is one more detail that needs to be looked at again in initComputed:

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  ...
  Object.defineProperty(target, key, sharedPropertyDefinition)
}Copy the code

The defineComputed method it calls creates a property of the same name in the current component instance for the calculated property, which is why the calculated property is a method by definition, but a property in practice. It recalculates and assigns the value to the newly created proxy property only when the data it depends on is updated and notified by the set method. That’s where the efficiency of calculating attributes is

Third, summary

Writing source code analysis is difficult to cover all aspects, after all, can not always post code, how to post the minimum amount of code to clarify the problem, this is still the direction of efforts. Before reaching this goal, can only through the way of asking questions, welcome to comment, try to answer

This article was also published in the front of the public account Feimai: