Computed

Initialization process

Vue’s computed properties are initialized in both initState and vue.extend

In initState

export function initState (vm: Component) {
  // ...
  
  const opts = vm.$options
  // ...
  
  if (opts.computed) initComputed(vm, opts.computed)
  // ...
}
Copy the code

If vm.$options has computed, initComputed is called

// For computed Watcher, the lazy value is true
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // Create the VM._computedWatchers object
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  // Walk the attributes of computed
  for (const key in computed) {
    // Get the attribute value
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// Create a Watcher for computed
      / / create computedWatcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // When the component function (Sub) is created using the extend method, the prototype of Sub has been mounted with computed properties
    // In this case, we only define the computed properties defined at instantiation time. For example, the computed properties of the root component
    // Ensure that the attribute name of computed is not the same as that of data and props
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
Copy the code

InitComputed creates a Computed Watcher for each Computed property. The point to note here is that for Computed Watcher, options.lazy is true and the Computed property is assigned to the getter property of the Watcher instance. The created Computed Watcher is added to vm._computedWatchers. Next, defineComputed is executed to add the response

Consider the difference between Computed Watcher and Render Watcher

// The code inside the Watcher class

this.lazy = !! options.lazy// ...

this.dirty = this.lazy
// ...

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

For Computed Watcher, the lazy value is true and the dirty value is true. Because lazy is true, the this.get() method is not executed during the creation of Computed Watcher. The return value of the evaluated property will not be retrieved.

The component’s Render function is executed by executing this.get() during the creation of Render Watcher

The Vue. The extend

if (Sub.options.computed) {
  initComputed(Sub)
}
Copy the code

Vue.extend performs the initComputed function

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    // Mount the computed properties of the component to the prototype of the component function
    When instantiated, it can be accessed using this.key
    defineComputed(Comp.prototype, key, computed[key])
  }
}
Copy the code

The initComputed function executes the defineComputed method on all computed properties.

DefineComputed defined in SRC/core/instance/state. Js

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // True if not SSR
  constshouldCache = ! isServerRendering()/* * userDef may be a function or an object with getter and setter properties */
  // Set the fetching descriptor
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    // Set the saving descriptor
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // An error is reported when assigning a value to a evaluated property if no setter method is set for the evaluated property key
  if(process.env.NODE_ENV ! = ='production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`.this)}}// Add interception to allow developers to access this.key(vm.key)
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

The defineComputed method adds all computed properties to the VM/sub.prototype via object.defineProperty and sets the return value of the createComputedGetter function to take the descriptor; Sets the set method that evaluates the property as a storage descriptor

Now let’s look at createComputedGetter

function createComputedGetter (key) {
  return function computedGetter () {}}Copy the code

Look at the internal logic of the createComputedGetter function in a moment, but for now it returns a function that is fired when the evaluated property is retrieved

summary

componentcomputedThe initialization

For component computed initialization, all computed properties in the component are added to the prototype Object of the component constructor using the object.defineProperty method and access descriptors are set when the component constructor is created.

When a component instance is created, a Computed Watcher is created for each Computed property and the Computed property is copied to the getter property of the Watcher instance. In the development environment, the system determines whether the key in computed is the same as the key in data and props.

The root instancecomputedThe initialization

To initialize the root instance computed, it is easy to get the computed properties, create a computed Watcher for each key of computed, and mount all the computed properties to the component instance using the object.defineProperty method. And set the access descriptor.

Principle of response

When the component executes the render function, if a computed property is used, it fires the getter for that computed property, which is the return value in the createComputedGetter above

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // Do the dependency collection only once
      if (watcher.dirty) {
        // Execute the defined computed property function
        watcher.evaluate()
      }
      if (Dep.target) {
        // Add the render watcher to the deP of the dependency property. When the dependency property is changed, trigger the component update through the Render watcher get method
        watcher.depend()
      }
      return watcher.value
    }
  }
}
Copy the code

First, a Computed Watcher is obtained based on the key. Because watcher.dirty is true during initialization, the watcher.evaluate() method is executed

evaluate () {
    this.value = this.get()
    this.dirty = false
}
Copy the code

The evaluate method executes the this.get() method, gets the return value of the evaluated property, and sets the current Watcher’s dirty to false, preventing multiple executions of the this.get() method.

The GET method was seen in the Principles of Responsiveness section

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
    } finally {
      // ...
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
Copy the code

For a Computed Watcher, run this. Getter to compute the attribute value and obtain the result value. Then, exit the stack, add the deP of the dependent attribute to depIds and deps, and return the result.

When this. Getter is executed, the variable value of the dependent property in the Computed property is obtained, and the getter of the responder variable is triggered to add Computed Watcher to the dep.subs of the responder variable.

Return to createComputedGetter, where dep. target refers to the component’s Render Watcher, because the component’s Render Watcher is pushed when the component’s Render function is executed. When the value of the Computed attribute is obtained, the compute Watcher is pushed into the stack and Computed Watcher is removed from the stack. Therefore, Dep. Target refers to the Render Watcher of the component. Next, execute the Depend method of Computed Watcher

depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
}
Copy the code

The depend method iterates over deps, which is an array of Dep instances, and executes the depend method for each Dep instance, adding the component Render Watcher to the dep.subs of all the attributes that the attribute depends on

Watcher. depend returns the return value of the evaluated property after execution, and the collection of dependencies is complete

summary

The dependency collection process for computing attributes is really a dependency collection process for the responsive attributes that are used

Computed Watcher is added to the dep.subs of responsive properties during the execution of the Computed property when it is used in the component’s Render function. Add the component’s Render Watcher to the responsive property dep.subs as well

update

The setter method for the dependent property is triggered when a responsive property change for a property dependency is evaluated

set: function reactiveSetter (newVal) {
  // ...
  
  dep.notify()
}
Copy the code

The setter method notifies all Watcher, including Computed Watcher and Render Watcher, of updates. Call Watcher’s update method

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}Copy the code

For Computed Watcher, set dirty to true. Render Watcher executes the run method of the Watcher instance, re-executing the component’s Render function to update the return value of the evaluated property

The effect of dirty is to reevaluate only when the relevant reactive properties change. If the return value of a evaluated property is repeatedly retrieved, it will not be re-evaluated as long as the responsive property has not changed.

That is, when the responsive property changes, the setter of the responsive property is triggered and the dirty value is set to false for Computed Watcher. When obtained again, the latest value is obtained and the Watcher is added to the dep.subs of the responsive property

Watch

The initialization of watch also takes place in initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  / /...
  
  
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Add an _Watchers array to the VM to hold the current component’s watch. The initWatch method is then called to initialize all watch properties

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    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

InitWatch calls createWatcher for all watches

function createWatcher (
  vm: Component,
  expOrFn: string | Function, handler: any, options? :Object
) {
  if (isPlainObject(handler)) {
    // Processing parameters
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    // Handler can be a method name
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code

CreateWatcher takes the callback function and calls the vm.$watch method

  Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    // If the listener is set by this.$watch, then createWatcher is executed to get the callback function
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // If user is true, the Watcher created is a user Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        // If options. Immediate is true, the callback function is executed immediately
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}// Return a function to cancel the listener
    return function unwatchFn () {
      watcher.teardown()
    }
  }
Copy the code

Value.prototype.$watch: create a User Watcher and check whether options. Immediate is true. Finally, a function is returned to cancel the listening.

This is the end of the watch initialization process

summary

The ultimate goal of the watch initialization process is to create a User Watcher for each watch. During the creation process, the monitored properties are collected (described in the next section).

Watch the update

During initialization, a User Watcher will be created for each watch, and the dependency collection of the monitored properties will be done during the creation

const watcher = new Watcher(vm, expOrFn, cb, options)

Let’s look at the parameters first

Name of the attribute monitored (XXX,'xxx.yyy'Options {user:true, deep: [custom configuration item], async: [custom configuration item]}Copy the code

Procedure for creating a User Watcher

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      // If Watcher is not rendered, _watcher will not be mounted to the VM
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      /** * computedWatcher lazy is true * userWarcher user is true * deep and sync are configuration items of watch */
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } 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(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // User Watcher's expOrFn is a string representing the name of the property monitored
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop 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
        )
      }
    }
    // For computed Watcher, the lazy attribute is true. That is, the GET method is not executed immediately
    Render Watcher's lazy property is false, and the get method is immediately executed, returning undefined
    // If the user Watcher's lazy property is false, the user Watcher will immediately execute the get method and return the value of the property being listened on
    this.value = this.lazy
      ? undefined
      : this.get()
  }
Copy the code

User Watcher has two properties, deep and async, in addition to the true User property. These two properties are the configuration items of watch.

A given Watcher is instantiated to determine the type of the expOrFn parameter. For User Watcher, the expOrFn is the name of the property listened on, which is a string, so the parsePath method is executed.

export function parsePath (path: string) :any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('. ')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

The parsePath method is based on. Cut the string into an array of strings and return a function that is assigned to the User Watcher’s getter property. Internally, the function gets the attribute values of all the elements in the array in turn and returns the attribute values

Assuming the name of the property to be listened on is A.B.C, this function will fetch the values of this.a, this.a.b, and this.a.b.c in sequence

Go back to User Watcher, where the this.getter has been assigned, and the this.get method is executed; That is, only Computed Watcher is created without the this.get method

Look at the get method again

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {} finally {
      if (this.deep) {
        // If deep is true and the property being listened on is an object, then all properties in the object are collected once
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
Copy the code

The logic of the get methods is the same whether they calculate properties, data, props, and watch.

  1. The currentWatcherInto the stack
  2. performthis.getter(Each type ofWatcherthegetterProperties of different)
  3. performtraverseMethods (onlydeepfortruetheUser WatcherWill perform)
  4. The currentWatcherOut of the stack
  5. To deal withWatcherthedepsattribute
  6. returnvalue

In the case of User Watcher, the getter is the return value of parsePath. During the execution of the getter, it will get the value of the property being listened on, which will trigger the getter method of the property being listened on, adding User Watcher to the property’s dep.subs.

After the above execution, the traverse method is executed to determine if deep is true. If true, the traverse method is executed

const seenObjects = new Set(a)export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
Copy the code

Traverse methods are simple. If the property being listened to is an object, they call all the properties of the object, triggering dependency collection for all the properties. Add User Watcher to the dep.subs for each property so that when a property is modified, the setter for the property is triggered, triggering the watch callback

Triggered the callback

When the value of the property is changed, the setter for the property is triggered, notifies all watchers in dep.subs of the update, and executes the watcher.update method

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}Copy the code

If User Watcher’s sync property is true, the run method is executed immediately. If sync is false, the run method of User Watcher is executed in the next task queue using queueWatcher(this)

Take a look at the run method first

  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        isObject(value) ||
        this.deep
      ) {
        // Why you can get old and new values in the parameters of the callback function when adding a custom watcher
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
Copy the code

For User Watcher’s run method, it first calls this.get() to reset the dependency collection for the monitored property and get the latest value. If the latest and old values do not want to wait, the callback function is called and the new and old values are passed in

The above judgment logic in addition to determine whether the old and new values to will determine isObject (value) | | this. Deep, this is because if the monitor properties is an object/array, modify the object/array properties, old and new values are the same, so in order to prevent this kind of situation leads to the callback does not perform, To add this logic

Let’s see how User Watcher is called in queueWatcher; Normally, being identical with data is to add User Watcher to the queue and ensure that each User Watcher in the same queue is unique

Different situation

inwatchCallback to modify the value of another property being listened on

His execution logic is as follows:

export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) = > a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // Execute the component's beforeUpdate hooks, with parents before children
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break}}}Copy the code

FlushSchedulerQueue method is executed on the next queue

  • flushingSet totrueIs being updated in the queueWatcher;
  • Queue sort, guaranteeWatcherUpdate order;
  • Traverse the queue, updating all in the queueWatcher
  • has[id] = null, will be updatedWatcherfromhasRemoved;
  • performUser WatchertherunMethods;
  • Do the first onewatchIn which the value of the property being listened on is modified and the property is triggeredsetterWill listen for this propertyUser WatherthroughqueueWatcherTo the queue, and now it’s different
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if(! flushing) { queue.push(watcher) }else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) { waiting =true

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

Because I already set flushing to true up here, so I’m going to do else logic; The else logic is to walk through the queue and add Watcher to the corresponding location. The location logic is as follows

1. Component updates are from parent to child. (because the parent component is always created before the child component) 2. User Watcher executes 3. If a component is destroyed during a parent Watcher execution, its Watcher execution can be skipped, so the parent Watcher should be executed firstCopy the code

NextTick (flushSchedulerQueue) is not executed again because waiting is already true. Instead, we return to the flushSchedulerQueue method to continue the loop

inwatchCallback to modify the value of the currently monitored property

FlushSchedulerQueue: flushSchedulerQueue: flushSchedulerQueue: flushSchedulerQueue

Because the newly added User Watcher is the same Watcher as the User Watcher that has just been executed, the if condition that is triggered next is true in the development environment

if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
  circular[id] = (circular[id] || 0) + 1
  if (circular[id] > MAX_UPDATE_COUNT) {
    warn(
      'You may have an infinite update loop ' + (
        watcher.user
          ? `in watcher with expression "${watcher.expression}"`
          : `in a component render function.`
      ),
      watcher.vm
    )
    break}}Copy the code

In this case, circular[ID] will increase the number of circular[ID] by one and repeat the preceding logic until the total number is greater than MAX_UPDATE_COUNT

conclusion

Difference between Computed and watch

computed

  • Depends on other attribute values, andcomputedThe value is cached
  • Get it next time when the value of the property it depends on changescomputedIt will be recalculated when the value is givencomputedThe value of the

watch

  • No caching, more observation
  • Each time the monitored data changes, a callback is performed for subsequent operations

Usage scenarios

  • Should be used when numerical calculations are required and other data are dependentcomputedBecause it can be usedcomputedTo avoid having to recalculate a value every time it is fetched
  • Used when you need to perform asynchronous or expensive operations as data changeswatch, and before getting the final result, you can set the intermediate state

For computed

During initialization, a Computed Watcher is created for each Computed property, all Computed properties are added to the prototype Object of the component instance/component constructor via object.defineProperty, and access descriptors are added to all Computed properties.

When a computed property is obtained, the getter of the computed property is triggered to calculate the value computed and the dirty is set to false. In this way, the cached value is directly returned when the computed property is obtained again. During the computation of a computed value, a computed Watcher is added to the Dep of the dependent attribute.

When the dependent attribute changes, the update of Computed Watcher is triggered. The dirty value is set to true, and the Computed value is recalculated the next time the dependent attribute is obtained

How does watch trigger a callback

During initialization, a User Watcher is created for each watch. If the watch’s immediate value is true, a callback is performed immediately. When User Watcher is created, the value of the property to be listened on is fetched once, which triggers the getter method of the property to be listened on, and adds User Watcher to the Dep instance of the property to be listened on.

When the monitored property changes, the User is notified that the Watcher is updated. If sync of the watch is true, the watch callback is immediately executed. Otherwise, the update method of User Watcher will be placed in the cache queue by nextTick. In the next event loop, the property value of the monitored property will be retrieved, and the new value will be determined whether the new value wants to wait, whether the deep value is set to true, and whether the monitored property is an object type. If so, the callback will be performed.