关于computed源码

问:下面代码从store中取值,如果template中没有使用msg, 初始化加载会打印吗?其他页面改变store的值,这里会执行打印吗?

computed: {
     msg() {
        console.log(this.$store.state.name)
        return this.$store.state.name
      }
}

答: 不会。
computed触发条件:
1.函数内依赖了vue的属性
2.这些属性发生了改变
3.这些属性被页面引用(触发计算属性的getter,比如打印这个计算属性)。
这三个条件同时满足,才会触发computed中定义的某个函数的回调

从以上场景来思考computed的执行

一、在初始化data后初始化computed,initState源码段
function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.data) { initData(vm);}
  if (opts.computed) { initComputed(vm, opts.computed); }
   ...
}

二、 initComputed初始化computed。
  1. 实例化Watcher,
    注意两个参数:
    (1)第二个参数就是computed中的函数体getter,在什么时机执行呢,后面我们到Watcher中去看
    (2)第四个参数传入了lazy: true,将所有的computed watcher存到this._computedWatchers,后面要用的
  2. 执行defineComputed
var computedWatcherOptions = { lazy: true };

function initComputed (vm, computed) {
  var watchers = vm._computedWatchers = Object.create(null);

  for (var key in computed) {
    var userDef = computed[key];
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
}
三、 defineComputed部分代码:利用Object.defineProperty使其变为响应式,并挂在vm上,get函数逻辑在createComputedGetter
var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

function defineComputed ( target, key,userDef) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop;
  }

  Object.defineProperty(target, key, sharedPropertyDefinition);
}
四、 createComputedGetter:返回一个函数给到get

(1)上面初始化中存的this._computedWatchers中如果有这个watcher,并且dirty就执行watcher.evaluate()得到value,并返回value(也就是watcher.value)
(2)记住它:这里有一个Dep.target,执行了depend。后面看到watcher代码我们继续看

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初始化变为了响应式数据且挂在到了this上。
2.要想computed的get被执行,那就需要这个响应式数据被使用。也就是在渲染到视图层时(或者this.msg)会触发get拿到最新的value。

重点:这里要注意一个条件:watcher.dirty 必须要dirty是true才会拿值。watcher.evaluate获取最新值就会执行computed的函数体getter。以下代码我们看看watcher部分源码
五、Watcher 部分
  1. 这里能看到第二参数expOrFn就是函数体getter。第四参数options中的lazy默认传进来是true,自然的this.dirty也是true。this.value就是undefined。证明初始化的时候不会执行函数体getter
class Watcher {
constructor ( vm, expOrFn, cb,options) {
    this.vm = vm;
    vm._watchers.push(this);
    if (options) {
      this.lazy = !!options.lazy;
    } 
    this.dirty = this.lazy; // for lazy watchers
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } 
    }
    this.value = this.lazy ? undefined : this.get();
  }

}

以上是Watcher的constructor,下面我们看看Watcher中的方法

  1. evaluate还记得吗,上面我们讲到:如果计算属性被使用,进入计算属性的get,watcher.evaluate在dirty为true的时候执行
    (1)通过Watcher中的get方法拿值: this.get(),在 this.get()中执行computed的函数体getter
    (2)拿到值后this.dirty变成了false
// class Watcher {
以下是Watcher中的方法
  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */。
  evaluate () {
    this.value = this.get();
    this.dirty = false;
  }

  1. 看看get方法
    (1) 在这里,函数体getter的执行了。执行函数体,就会触发依赖值的get进行计算拿到最新值
// class Watcher {
以下是Watcher中的方法
 /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
 // 将自身设置为订阅对象
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  }

到这里我们就明白了,初始化的时候传入lazy,lazy = dirty = true。所以渲染到视图层时会触发get会执行取值,然后dirty变为了false。
那么想要再次执行就必须有一个方法去将dirty变为true,我们找一找

  1. 上面代码有 pushTarget(this),执行函数体getter后,再 popTarget()

注意这里push之后又pop删掉了,Dep.target会变化

Dep.target = null;
const targetStack = [];

function pushTarget (target) {
  targetStack.push(target);
  Dep.target = target;
}

function popTarget () {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

5.再看看Dep。原来是一个发布订阅器,可以看到最后执行订阅对象的update方法

原来 pushTarget(this)是把watcher自己当作了订阅对象,给别人订阅,这里watcher就是Dep.target。我们看代码:下面有一个Dep. depend,如果执行就会跑watcher上的addDep方法

class Dep {
  constructor () {
    this.id = uid++;
    this.subs = [];
  }

  addSub (sub) {
    this.subs.push(sub);
  }

  removeSub (sub) {
    remove(this.subs, sub);
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    if (!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((a, b) => a.id - b.id);
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

看看watcher上的addDep方法,参数是订阅器Dep。最后执行Dep上的addSub将watcher加入到了订阅器

// class Watcher {
以下是Watcher中的方法
/**
   * Add a dependency to this directive.
   */
  addDep (dep) {
    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);
      }
    }
  }

看着这里你应该明白了:
(1)Watcher是监听器,Vue会提供观察者去订阅他,如果观察者发现需要更新某个操作,会通知Watcher,watcher会执行update进行更新。
(2)computed的watcher参数不一样,传入了一个lazy:true。所以lazy:true具有代表性

我们可以想到:这里的update肯定要将dirty变为了true,不然无法执行我们上面说到的执行函数体getter

// class Watcher {
以下是Watcher中的方法, 只有computed的watcher的lazy默认传入的是true
update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }

现在,只要在适当时机触发Dep. notify() 就能实现update函数执行将dirty更新为true。

思考

(1)谁是观察者来执行Dep. notify() ?自然是依赖的数据变化了,就去notify,将dirty改为true
(2)什么时候执行Dep. depend?想一想应该也是观察者,也就是依赖的数据变化,我们往下一步步看找答案
(3)还记得上面的createComputedGetter吗,代码里面有个watcher.depend。它做了什么呢?

computed的get
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
    }
  }
}
// class Watcher {
以下是Watcher中的方法
 /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }

先解决第一个和第二个问题:依赖的数据是响应式的,那initData时候,响应式数据的set中一定有notify的执行。看下面代码验证一下

六、我们回到initState代码
  1. 初始化data都会走到observe()
function initState (vm) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.data) { initData(vm); } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
}


function initData (vm) {
 // observe data
  observe(data, true /* asRootData */);
}
  1. 再看看Observer:所有挂在的数据都走到了defineReactive$$1,还判断了数据是数组的情况,observeArray是循环再执行observe,这里我们只看普通情况
class Observer {
 
   // number of vms that have this object as root $data

  constructor (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  }
}
  1. 继续看defineReactive$$1, 确实在set里面看到了notify,get里面也有Dep.depend()。
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  const dep = new Dep();
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

(1)从get中可以看到,首先实例化一个发布订阅器 const dep = new Dep()

(2)当响应式数据被触发会判断Dep.target,Dep.target如果有值(上面说过,它就是一个watcher),就执行订阅器的dep.depend()

(3)对第二个问题解决:什么时候执行Dep. depend? 在上面代码中看到了。

class Dep {
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
}

执行watcher的addDep方法,
this.newDeps 存储了该响应式数据的dep实例
实际上就是给第一步实例的dep注入了watcher,以便下面set中notify的时候执行指定的watcher的update方法,这里就建立了对应关系。

// class Watcher {
以下是Watcher中的方法
/**
   * Add a dependency to this directive.
   */
  addDep (dep) {
    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);
      }
    }
  }

(4)上面第三个问题问到watcher.depend。它做了什么呢?这需要仔细看看

我们捋一捋整个流程的顺序

第一步:进入计算属性的get执行以下代码,也就是createComputedGetter

      if (watcher.dirty) {
        watcher.evaluate();
      }
   这里还不慌执行,我们在第四步执行
    if (Dep.target) {
        watcher.depend();
      }

第二步:执行watcher.get() , 并执行函数体getter: this.getter.call(vm, vm)
执行的时候就会进入到依赖的数据的get中,也就是defineReactive$$1 上面执行dep.depend(),我们知道this.newDeps搜集了响应式数据的发布订阅器。依赖多少响应式数据就搜集多少

当前的Dep.target(当前计算属性的watcher)通过this.newDeps搜集了所有的响应式依赖

  get () {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } finally {
      popTarget();
      this.cleanupDeps();
    }
    return value
  }

第三步:执行上面的finally,this.cleanupDeps。this.deps拿到了响应式依赖

 cleanupDeps () {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }

第四步:执行完watcher.get。终于来到重点环节.现在知道了this.deps的来源了。现在回到了计算属性的get来执行这一段

  if (Dep.target) {
        watcher.depend();
     }

等等,watcher.depend循环执行了所有依赖项的发布订阅器Dep.depend()方法。这是什么意思?this.deps里面已经是所有的依赖了,再执行一遍?

这里有个巧妙的转变:还记得我们上面说的push之后又pop删掉了,Dep.target会变化吧。它变为了谁,我们继续看

通过断点,我发现targetStack里面还有其他东西。 Dep.target变为了 :this.update(),这玩意儿一看就知道是更新用的。

企业微信截图_36d70ee9-6e95-44d3-9819-d4322095ca03.png

通过全局搜索new Watcher才知道:有三类watcher
(1)computed的watcher。默认lazy并不马上执行get,这个我们是知道的
(2)自定义watcher,也就是我们自己写的this.$watch,马上执行get,这里暂且不论。代码为证:

Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    const vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`;
      pushTarget();
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
      popTarget();
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };
}
(3)render watcher 触发渲染视图用的,马上执行get。我们看看简化代码mountComponent
function mountComponent (vm,el,hydrating){
   let  updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
 // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
return vm
}

明白了:初始化的时候mountComponent,执行了new Watcher中的get。在targetStack数组中,第一个元素就是会引发渲染的 updateComponent的watcher。

所以:当计算属性的get执行到下面代码时

Dep.target此时为render watcher。
// 如果页面上没有使用计算属性,
//但是又通过this.computed取值触发计算属性的get
//这里就会是undefined
  if (Dep.target) {
        watcher.depend();
     }
依次执行下面的代码
class Watcher {
  depend () {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }
}
此时Dep.target 就变为了updateComponent的watcher。
所以此时addDep是给render watcher添加依赖
class Dep {
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
 }

1个Dep可以被多个Watcher订阅,1个Watcher也可以订阅多个依赖。通过遍历计算属性watcher的deps,让render watcher去订阅他们: 计算属性的值变化了,就会发render watcher 的update执行以触发更新视图。

总结一下整个流程:
  1. 初始化的时候lazy = dirty = true。将计算属性设置为响应式并重写了get

  2. 当页面上要使用计算属性时,会进入到计算属性的get,get中dirty = true会进入到watcher.get并执行pushTarget(this)。此时全局的Dep.target就是计算属性的watcher

  3. 此时执行计算属性的函数体getter,里面有一个响应式数据,比如:this.$store.state.name.就会进入到该响应式数据的get。这个get就是我们上面说的过程:发现Dep.target(当前watcher)有值,就将此watcher放入了实例的Dep(订阅watcher)

  4. 在set的时候如果值没变化,直接return。有变化就利用dep. notify通知watcher执行update 把dirty设置为true。这里的响应式数据返回一个新值

  5. 响应式数据拿到新值,计算属性也就拿到了新值。然后popTarget。 此时的Dep.target变为了render watcher 。然后执行watcher.depend(),给render watcher 添加依赖。触发变化后,引起试图更新

尾声:update更新部分,怎么合并更新? 有时间再更新
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容