计算属性

155 阅读2分钟

版本:vue@2.6.10

0-计算属性的诞生(initComputed)

初始化 options中的 computed 属性。

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  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) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    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)
      }
    }
  }
}
1-在实例上新建一个 _computedWatchers 属性用于存放computed的watcher实例
2-获取computed的getter属性
3-判断环境是否是ssr
	-非ssr的话,用computed的getter属性生成watcher实例,并将computed对应的watcher实例存到实例上的_computedWatchers中,
        vm._computedWatchers[key] =new Watcher(vm, getter, noop, computedWatcherOptions)
4-对vm上的属性进行校验,因为我们要把computed属性也挂到vm实例上,所以computed名称不能与props,data中的属性值相同。
5-当校验通过,对computed的getter进行劫持,根据是否是服务端渲染,进行不同的劫持,
    -createComputedGetter  //非服务端渲染
	-createGetterInvoker   //服务端渲染
6-最后将computed对应的key添加到vm实例上

createComputedGetter

 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
      }
    }
1-对computed的属性劫持,是每当使用computed属性时(get),都会调用我们的劫持属性,
  劫持属性会对computedWatcher进行一些操作,在最后分析。
2-计算属性的watcher的dirty属性,是一个标志,当为false时,说明计算属性依赖的数据没有更新,
  只让依赖数据收集渲染watcher,将value直接返回,只有依赖数据更新时,dirty才为true,才重新计算值。

function createGetterInvoker(fn) {
    return function computedGetter () {
      return fn.call(this, this)
    }
  }     
1-服务端渲染,对computed的劫持,没有watcher????????????  

computedWatcher

watchers[key] = new Watcher(
        vm,                    //实例
        getter || noop,        //computed属性调用的函数
        noop,                  //空函数
        computedWatcherOptions //计算属性的特性值 { lazy: true }
      )

Watcher 构造函数

由于我们的watcher构造函数,被运用于三种watcher,我们的computedWatcher只走其中的一部分,因此简化为:

  var Watcher = function Watcher (
    vm,              //实例
    expOrFn,         //computed属性调用的函数
    cb,              //未传
    options,         //{ lazy: true }
    isRenderWatcher  //未传
  ) {
    this.vm = vm;
    vm._watchers.push(this);
    this.lazy = !!options.lazy;
    this.cb = cb;
    this.id = ++uid$1; // uid for batching
    this.active = true;
    this.dirty = true
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression =  expOrFn.toString();
    this.getter = expOrFn;
    this.value = this.lazy
      ? undefined
      : this.get();
  };
1-我们在初始化computed时在vm._computedWatchers保存的watcher就是上面这些属性
   (计算属性的watcher主要标志是dirty,lazy,初始化时都是true)。
3-由于我们对computed函数的get做了劫持处理,在使用computed属性时,
  调用的是createComputedGetter的返回函数,会将我们保存在vm._computedWatchers中对应的computedWatcher取出来。 
由于我们的watcher.dirty是true,运行watcher.evaluate()

watcher.evaluate()

  Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };
1-运行this.get是获取计算属性的最新值,然后设置计算属性watcher的状态dirty为false
  【证明这个计算属性现在是最新的数据。】

Watcher.prototype.get

  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };
1- 运行计算属性watcher的get方法,
2- 将计算属性的watcher赋值给Dep.target
3- 运行计算属性的函数,将值赋值给value
    - 如果在计算属性的函数内,依赖了data,props,methods的话,
       由于Dep.target是计算属性的watcher,那么在收集依赖时收集的也是计算属性。【关于依赖互相收集的过程后续单独分析】
4- 最后将计算属性watcher出栈,Dep.target还原为外层watcher,    
5- this.cleanupDeps()
    -对depIds newDepIds newDeps deps进行维护。防止重复收集?????

在运行完get后,设置dirty为false,然后继续运行createComputedGetter。因为在get函数中,我们的计算属性watcher已经入栈【成为Dep.target】又出栈【Dep.target又变为外层的渲染watcher】被依赖的响应式数据收集,所以,现在的Dep.target是外层的渲染watcher,继续运行

watcher.depend()【此时的watcher是计算属性watcher】,在depend函数中,是调用我们watcher收集的dep的depend函数,作用是调用Dep.target的addDep函数,addDep的作用是watcher将dep收集并且dep同时将watcher收集。【这是一个以来相互收集的过程,详情在另一篇。】这样,渲染watcher和计算属性所依赖的数据也进行了相互收集。

最终结果是计算属性所依赖的数据(data,props,methods)即收集了计算属性watcher,也收集了渲染watcher。

当计算属性所依赖的数据发生变化时,会通知收集的watcher【计算属性watcher,渲染watcher】更新(watcher.update()),

Watcher.prototype.update()

 Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };
1-由于的计算属性的watcher的lazy属性为true,所以计算属性的watcher运行update只是更新了一下自身状态,
  将dirty设置为true【证明这个计算属性脏了】。
2-渲染watcher的更新是queueWatcher(this)。

queueWatcher(watcher)

 var MAX_UPDATE_COUNT = 100;
  var queue = [];
  var activatedChildren = [];
  var has = {};
  var circular = {};
  var waiting = false;
  var flushing = false;
  var index = 0;
  /**********/
  function queueWatcher (watcher) {
    var id = watcher.id;
    if (has[id] == null) {               //防止同一个watcher入栈多次进行更新
      has[id] = true;                    //收集入栈的watcher的id,进行标记
      if (!flushing) {                   //判断是否已经进行更新,flushing为false是未在更新
        queue.push(watcher);
      } else {
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var 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 ( !config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }
1-queueWatcher的作用是将watcher入栈进行统一更新。
2-当前还没有进行更新的话,就将watcher放入更新队列里。
3-在更新时会对watcher进行排序后更新,排序是为了保证:     【flushSchedulerQueue内为执行更新】
	-组件从父级更新到子集
	-userWatcher在renderWatcehr前更新
	-当某个组件在父组件的观察期间被销毁,他的观察者就可以不用执行
4-已经开始更新的话,由于我们的watcher是进行排序后更新的,将我们的watcher插入排序的队列里	
5-初始waiting为false,设置为true,config.async【异步更新,初始为true】运行   	 
  nextTick(flushSchedulerQueue)

nextTick

  function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }
function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }
1-nextTick将我们的flushSchedulerQueue进行包裹后放入callbacks里。
2-初始pending为false,设置pending为true执行timerFunc()
3-当运行环境支持Promise时,timerFunc是一个promise函数,执行flushCallbacks函数,
  将执行我们放进callbacks的函数【也就是经过包装的flushSchedulerQueue】

flushSchedulerQueue

  function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();           //获取当前时间   
    flushing = true;                            //是否已经开始更新的标志
    var watcher, id;
    // Sort queue before flush.
    // This ensures that:
    // 1\. Components are updated from parent to child. (because parent is always
    //    created before the child)
    // 2\. A component's user watchers are run before its render watcher (because
    //    user watchers are created before the render watcher)
    // 3\. If a component is destroyed during a parent component's watcher run,
    //    its watchers can be skipped.
    queue.sort(function (a, b) { return a.id - b.id; });  //对队列里的watcher进行排序

    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      if (watcher.before) {   //判断是初始挂载还是更新 更新运行beforeUpdate钩子
        watcher.before();
      }
      id = watcher.id;
      has[id] = null;                      //删除记录中已经执行更新的watcher
      watcher.run();
      // in dev build, check and stop circular updates.
      if ( 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
        }
      }
    }
    // keep copies of post queues before resetting state
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();

    resetSchedulerState();

    // call component updated and activated hooks
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
      devtools.emit('flush');
    }
  }
1-最终是运行watcher.prototype.run函数再次进行收集挂载【其中包括虚拟dom的对比更新,另一篇学】

vue官网对于 computed 属性的列举

// 仅读取
    aDouble: function () {
      return this.a * 2
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }