vue2从数据变化到视图变化:计算属性(computed)

434 阅读3分钟
new Vue({
  el: '#app',
  data: {
    baseData: 1,
  },
  computed: {
    computedData: function () {
      return this.baseData + 1
    }
  },
  methods: {
    changeData() {
      this.baseData = 2;
    }
  }
})

如上,计算属性computedData依赖于this.baseData

一、计算属性首次执行

1、initComputed

在执行new Vue的过程中,执行到initState,当满足if (opts.computed) { initComputed(vm, opts.computed)}时,执行initComputed初始化计算属性。

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    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 internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in 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)
      }
    }
  }
}

通过循环的方式在const watchers = vm._computedWatchers = Object.create(null)实例化Watcher类,watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) 通过传入参数const computedWatcherOptions = { lazy: true }标明当前watcher是计算属性。

实例化完计算属性后,如果当前的key不在vm实例上,则响应式处理当前计算属性的key:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const 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)
}

isServerRenderingfalse,所以shouldCachetrueget的时候执行的是函数createComputedGetter

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

2\

(1)watcher.evaluate()手动watcher计算

watcher.dirtytrue的时候,执行watcher.evaluate()手动计算:

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

这里执行到this.get,进而执行到computer watcher的回调函数:

function () {
  return this.baseData + 1
}

这里访问到了this.baseData,会触发this.baseDataget,进而在其锁定的发布者depsubs中推入的计算属性computer watcher

(2)watcher.depend()手动依赖收集

Dep.targettrue的时候,执行watcher.depend()手动依赖收集:

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

通过手动的方式,将当前watcher订阅的发布者deps进行循环,收集当前正在计算的watcher,这个例子中指的是渲染watcher

4、依赖收集总结

当执行_render获取vNode的过程中会访问到this.computedData,触发到get方法,即createComputedGetter中返回的computedGetter:

function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
(1)watcher.evaluate

watcher.evaluate的过程中会执行value = this.getter.call(vm, vm),进而访问到this.baseData的数据,进而触发get,执行dep.depend()计算属性依赖收集操作:
depsubs中推入computer watcher;

(2)watcher.depend

watcher.depend的过程会触发this.deps[i].depend()收集的过程,例子中的this.baseData锁定的dep可以再次收集到渲染依赖:
depsubs中再次推入render watcher; 经过watcher.evaluatewatcher.depend的执行,this.baseData的依赖subs中就有computer watcherrender watcher

二、计算属性依赖数据变化

this.baseData变化时,会执行dep.notify方法:

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

这里depsubs有两个元素,浅拷贝var subs = this.subs.slice()的方式获取到subs,循环执行update方法:

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

第一次循环computer watcherthis.lazytrue,因此this.dirty = true
第二次循环render watcher会通过异步的方式执行queueWatcher(this),等所有的同步结束后,会执行到render watcher的执行,即:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

通过_render的方式获取vNode的时候又会访问computedData,进而执行计算属性响应式处理过程中的get

function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }

这里依然会执行到计算属性的重新计算和依赖的收集,收集的方法会执行到:

/**
 * Add a dependency to this directive.
 */
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

在首次渲染的时候已经执行过依赖收集,因此this.depIds中已经存在id,这里跳过执行。

小结:

计算属性也是Watcher类的实例,在访问所依赖的数据时被收集,在依赖的数据发生变化时依然触发dep.notifycomputer watcher主要修改this.dirty = true,使得访问计算属性的时候可以手动计算修改后的值。