Learn the calculated data in Vue from the perspective of source code

Posted by adige72 on Tue, 08 Mar 2022 10:54:16 +0100

computed: calculated attribute, which is generally used when a variable depends on the change of multiple variables or needs some processing.

  • The value is responsive
  • The data is cached

Calculation attribute: cache based on their response type. It will be re evaluated only when the related response type dependency changes.

The following is the analysis relative to the source code, with explanations in the comments:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

1,initComputed(vm, vm.$options.computed)

  • Generate an empty object_ computedWatchers and mount them in the vue instance
  • Get the getter of computed (either the function that defines the value for computed itself or the get method must exist in the object)
  • Allocate a watcher instance for each key of the computed object and mount it to the_ computedWatchers
  • Judge that each key and data or prop will not have the same name, otherwise an error will be reported
  • Execute defineComputed
var computedWatcherOptions = { lazy: true };

function initComputed (vm, computed) {
  //① Generate an empty object_ computedWatchers and mount them in the vue instance
  var watchers = vm._computedWatchers = Object.create(null);
  //② Get the getter of computed (either the function that defines the value for computed itself or the get method must exist in the object)
  for (var key in computed) {
    var userDef = computed[key];
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        ("Getter is missing for computed property \"" + key + "\"."),
        vm
      );
    }
    //③ Allocate a watcher instance for each key of the computed object and mount it to the_ computedWatchers
    //Different from using watcher in other places, the configuration parameter {lazy: true} is passed in, so that it will not be evaluated immediately when instantiating, but only when accessing it
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // {lazy: true}
      );
    }
    // ④ Execute defineComputed
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== 'production') {
    // ⑤ Judge that each key and data or prop will not have the same name, otherwise an error will be reported
      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);
      }
    }
  }
}

2. defineComputed(vm, key, userDef), userDef is function or object

  • Reset the get and set methods for each calculated key through defineProperty()
  • If the key is judged to be function, set it to noop, and get executes createComputedGetter() or createGeeterInvoker() through cache judgment
function defineComputed (
  target,
  key,
  userDef // Calculate the value corresponding to the attribute
) {
  // true in browser environment
  var shouldCache = !isServerRendering();
  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;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  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
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

3. createComputedGetter(key), which implements the key functions of cache

  • Take out the corresponding watch instance from computedWatchers and return watch Value value.
  • Need to pass watch Dirty (data dependency change), execute watcher Evaluate() recalculates the obtained value
function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

For example, when the render function accesses the value that computed depends on, it will trigger the getter method of rendering computed watcher to execute the watcher depend().

 depend(){
   if(this.dep && Dep.target) {
     //The render watcher subscribes to the changes of the computed watcher.
     this.dep.depend() 
   }
 }
 evaluate () {
    if(this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value;
  }

4. computed watcher object (different from rendered watcher)

  • Initialization: lazy is assigned to dirty, so it will execute evaluate() once, execute get to change the value value, and then set dirty to false. Subsequent access to the changed value will not be recalculated, and the value will be taken directly
  • Collect responsive dependencies, and then execute watch when the dependency data changes Update method. If dirty is set to true, get() method will be called to calculate update value
  • The computed watcher does not evaluate immediately and holds a dep instance at the same time.
constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}  

**Summary: * * computed implementation is mainly to generate a vue instance_ computedWatchers object, through which all subsequent accesses are made. Each key value in the object is a watcher. When accessing, the watcher returns the value. Whether to recalculate is controlled by dirty. Each dependency change will trigger the watcher's update() to change the dirty value. The value will be recalculated in the next access, otherwise the previous value will be returned directly.

Topics: Vue Vue.js