Learn about Vue - [how Vue implements responsive]

Keywords: Javascript Attribute Vue

Written in the front: This article is a summary of personal daily work and study, non authoritative information, please bring your own thinking ^ - ^.

When it comes to response, we will first think of the data attribute in the Vue instance, for example: reassign a certain attribute in data. If the attribute is used in page rendering, the page will automatically re render. Here, data is used as a starting point to see how the response in Vue is realized.

Vue instance creation phase

When creating a Vue instance, a core method is executed: initState, which initializes methods / props / methods / data / calculated / watch. At this time, we only focus on data initialization:

function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  ...
  
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    
    ...
    proxy(vm, '_data', key)
    ...
    
  }
  
  ...
  
  observe(data, true)
}

Some codes which are not related to the current research are omitted in the code, which is represented by...
You can see that this method mainly does two things:

  1. Proxy data to VM. U data, that is, accessing the attribute key vm[key] in data will trigger getter, return VM. U data [key], and assign the same value. The function is obvious: in the future, when we want to access / assign a certain attribute key in data, we can directly use this[key] so that we don't need this. \
  2. Execute the observe(data, true) function, traverse every attribute in data, and set it as responsive through definedobject, that is, in normal use scenarios, when we access this[key] (where key is an attribute in data), we will trigger the getter to enter and access this. \

proxy code:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

observe Code:

observe (value: any, asRootData: ?boolean): Observer | void {
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert && // Change the newly added attribute to reactive
    !isServerRendering() && // Non server rendering
    (Array.isArray(value) || isPlainObject(value)) && // Array or object
    Object.isExtensible(value) && // Extensible objects
    !value._isVue // Non Vue instance
  ) {
    ob = new Observer(value) // Create Observer instance
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this) // Value. Ob = this, and ob is an enumerable property
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys) // value.__proto__ = Array.prototype
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

function defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // Attribute names such as' $attr '
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val) // Continue to observe the value of the current property
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      
      ...
      
    },
    set: function reactiveSetter (newVal) {
      
      ...
      
    }
  })
}

Still can't escape paste code, but can't find a more intuitive explanation than the code...
However, the core method is defineReactive, which still uses defineProperty to set getter/setter for each property in VM. \
At this point, the initialization of data has come to an end.

Template compilation / Mount phase

This is the execution phase of Vue.prototype.$mount. In fact, this phase includes compiling the template, converting the compilation results to generate the render function, and mounting the execution of the render function.
In this stage, the operation of data only exists when the execution of the render function is mounted. The execution of the core function: new Watcher(vm, updateComponent, noop, null, true)
Watcher Code:

 class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function, // Callback function
    options?: ?Object,
    isRenderWatcher?: boolean // Watcher of instance when render ing
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    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
    this.deps = [] // Dependency list
    this.newDeps = [] // New dependency list
    this.depIds = new Set() // Rely on ids
    this.newDepIds = new Set() // New dependent ids
    // parse expression for getter
    // Wrap the expression expOrFn as a getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * In fact, the source code has been annotated. This is for render and dependency collection.
   */
  get () {
    pushTarget(this) // Assign Dep.target to the current Watcher instance
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // Here is the render function
    } catch (e) {
      ...
      
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget() // Eject Deptarget
      this.cleanupDeps() // The added dependency falls into this.deps, and this.newDeps is cleared at the same time.
    }
    return value
  },
  addDep (dep: Dep) { // Add the Dep instance to this.newDeps queue. The Dep instance here is generated from defining getter/setter for data attribute through defineReactive, that is to say, the Dep instance here corresponds to a data attribute.
    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)
      }
    }
  }
  ...
  
}

From the above code, we can see that the core of responsive correlation is the so-called "dependency collection", that is, during the execution of the render function, the data attribute required for page rendering will be read, which triggers the getter of the response data attribute. Remember the generation related to the data attribute getter function when the defineReactive function is executed in the omitted observe function. Code?

defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // Attribute names such as' $attr '
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  ...
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // In the $mount function, when new Watcher performs dependency collection, it has assigned Dep.target as Watcher instance.
        dep.depend() // The dep instance here corresponds to the current data attribute, and the current dep instance will be placed in the watcher's dependency list.
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // setter related code
      ...
      
    }
  })
 }
 
 // Dep.dependent Code:
...
  depend () {
    if (Dep.target) { // Where Dep.target has been assigned as a Watcher instance
      Dep.target.addDep(this)
    }
  }
...

Summary of the first rendering of a page

The first rendering of a page basically involves the above two major processes. Here, we mainly discuss them based on data.
new Vue(options) mainly includes:

  1. Transfer the data function to the data object and assign it to VM. \
  2. A layer of agent is used through defineProperty. Accessing vm[key] will return VM. \
  3. Traverse VM. ﹐ data, execute defineReactive function, add getter/setter for each attribute of VM. ﹐ data, collect dependency in getter, and notify response in setter;

$mount mainly includes:

  1. Compile template / generate render function;
  2. Through the instance Watcher object, when the render function is executed, the data attribute used for page rendering will be accessed, thus triggering the getter of VM. \

In the getter, add the dep instance corresponding to the current attribute to the deps list of the Watcher instance, and add the Watcher instance to the subs Watcher list of the dep;

When the data property value changes

Why does the data attribute change and the page will be re rendered and updated? A lot of foreshadowing has been done. Next, let's see what operations will be performed when the data property is changed.
Remember the setter set for VM. U data [key] through the defineReactive function mentioned earlier? The setter will be triggered when the data changes

    ...
    
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) { // If the values before and after the update are the same, return directly
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal) // If newVal is a reference type, its properties are also hijacked
      dep.notify() // This is the core of the property update trigger operation. It will notify Watcher to update accordingly.
    }
    
    ...
    
    // dep.notify method
    
    ...
    notify () {
      const subs = this.subs.slice() // This is the Watcher list.
      for (let i = 0, l = subs.length; i < l; i++) { // Notify each Watcher, execute its update method, and update accordingly
        subs[i].update()
      }
    }
    ...
    
    // Watcher.prototype.update method
    
    ...
    
    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) { // Synchronous update
        this.run()
      } else {
        queueWatcher(this) // This method is to execute the Watcher.prototype.run method in nextTick, that is to say, the data property update triggers the setter and then informs the Watcher to update. This process is usually not synchronous, but will be put into a queue, executed asynchronously, and landed in our use: we do not need to worry about serious performance problems caused by modifying multiple data properties at the same time, because it triggers The update is not performed synchronously; another point is that the get method will be executed in the Watcher.prototype.run method (remember this method when the first rendering is for dependency collection?). In this method, render will be executed to generate vnode, and of course, the attributes in data will be accessed. This is a dependency update process. Is it a closed loop?
      }
    }
  
    ...
    

queueWatcher(this)
This method is to execute the Watcher.prototype.run method in nextTick, that is to say, the data property update triggers the setter and then informs the Watcher to update. This process is usually not synchronous, but will be put into a queue, executed asynchronously, and landed in our use: we do not need to worry about serious performance problems caused by modifying multiple data properties at the same time, because it triggers The update is not performed synchronously;
The other point is that the get method will be executed in the Watcher.prototype.run method (remember this method when dependency collection is performed in the first rendering?) In this method, render will be executed to generate vnode, and of course, the attributes in data will be accessed. This is a dependency update process. Is it a closed loop?
In addition, we can't ignore that the updated hook function will be triggered during the execution of this method. Of course, we don't do in-depth research here, just do a general understanding, because there are many details in Vue, but it doesn't affect our understanding of the main process.

Last

Put two diagrams written in the debugg ing source code, only you can understand where you originally thought...


THE END

Posted by bmarinho on Wed, 23 Oct 2019 19:14:24 -0700