带你了解vue计算属性的实现原理以及vuex的实现原理

·  阅读 2649

computed的作用

在vue的开发中,我们不免会使用到计算属性,使用计算属性,vue会帮我们收集所有的该计算属性所依赖的所有data属性的依赖,当data属性改变时,便会重新获取computed属性,这样我们就不用关注计算属性所依赖的data属性的改变,而手动修改computed属性,这是vue强大之处之一。那么我们不免会产生疑问,computed属性为啥能随着data属性的改变而跟着改变的?带着这个疑问,我们来解析下vue的源码,看看它是如何实现computed的依赖收集。

整体流程

computed的依赖收集是借助vue的watcher来实现的,我们称之为computed watcher,每一个计算属性会对应一个computed watcher对象,该watcher对象包含了getter属性和get方法,getter属性就是计算属性对应的函数,get方法是用来更新计算属性(通过调用getter属性),并会把该computed watcher添加到计算属性依赖的所有data属性的订阅器列表中,这样当任何计算属性依赖的data属性改变的时候,就会调用该computed watcher的update方法,把该watcher标记为dirty,然后更新dom的dom watcher更新dom时,会触发dirty的computed watcher调用evaluate去计算最新的值,以便更新dom。
所以computed的实现是需要两个watcher来实现的,一个用来收集依赖,一个用来更新dom,并且两种watcher是有关联的。后续我们把更新DOM的watcher称为domWatcher,另一种叫computedWatcher。

image

initComputed

该方法是用来初始化computed属性的,它会遍历computed属性,然后做两件事:
1、为每个计算属性生成一个computedWathcer,后续计算属性依赖的data属性会把这个computedWatcher添加到自己订阅器列表中,以此来实现依赖收集。
2、挟持每个计算属性的get和set方法,set方法没有意义,主要是get方法,后面会提到。

function initComputed (vm, computed) {
  var watchers = vm._computedWatchers = Object.create(null);
  // 遍历所有的computed属性
  for (var key in computed) {
    var userDef = computed[key];
    // 每个计算属性对应的函数或者其get方法(computed属性可以设置get方法)
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    // ....
    if (!isSSR) {
      // 为每个计算属性生成一个Wathcer
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    if (!(key in vm)) {
      // defineComputed的作用就是挟持每个计算属性的get和set方法
      defineComputed(vm, key, userDef);
    } else {
      // ....
    }
  }
}
复制代码

defineComputed

如上面所述,definedComputed是挟持计算属性get和set方法,当然set方法对于计算属性是没什么作用,所以这里我们重点关注get方法,我们这里只需要知道get方法是触发依赖收集的关键,并且它把两种watcher进行了关联。

function defineComputed (
  target,
  key,
  userDef
) {
  var shouldCache = !isServerRendering();
  // 下面这段代码就是定义get和set方法了
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef;
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop;
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop;
  }
  //...
  // 这里进行挟持
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

createComputedGetter

createComputedGetter有两个作用:
1、收集依赖 当domWatcher获取计算属性的时候,会触发该方法,然后computedWatcher会调用evaluate方法,最终会调用computedWatcher的get方法(下面会分析),来完成依赖的收集 2、关联两种watcher
通过第一步完成依赖收集后,computedWatcher能知道依赖的data属性的改变,从而计算出最新的计算属性值,那么它是怎么让另外一个watcher,即domWatcher知道的呢,其实就是通过调用computedWatcher.depend方法把两种watcher关联起来的,这个方法会把Dep.target(就是domWatcher)放入到计算属性依赖的所有data属性的订阅器列表中。

通过这两个作用,当计算属性依赖的data属性有改变的时候,就会调用domWatcher的update方法,它会获取计算属性的值,因此会触发computedGetter方法,使得computedWatcher调用evaluate来计算最新的值,以便domWatcher更新dom。

function createComputedGetter (key) {
  return function computedGetter () {
    // 取出initComputed创建的watcher
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // 这个dirty的作用一个是避免重复计算,比如我们的模板中两次引用了这个计算属性,那么我们只需要计算一次就够了,一个是当计算属性依赖的data属性改变,会把这个计算属性对应的watcher给设置为dirty=true,然后
      if (watcher.dirty) {
        // 这个会计算计算属性的值,并且会调用watcher的get方法,完成依赖收集
        watcher.evaluate();
      }
      // Dep.target指向的是模板中计算属性对应节点的domWatcher
      // 这个语句的意思就是把domWatcher放入到当前computedWatcher的所有依赖中,这样计算属性依赖的data值一改,
      // 就会触发domWatcher的update方法,它会获取计算属性的值从而触发这个computedGetter,然后computedWatcher会通过调用evaluate方法获取最新值,
      // 然后交给domWatcher更新到dom
      if (Dep.target) {
        watcher.depend(); // 关联了两种watcher
      }
      return watcher.value
    }
  }
}
复制代码

接下来我们来分析下computedWatcher,看computed是如何借用computedWatcher来完成依赖收集的。

Computed Watcher

watcher是实现computed依赖的关键,它的第二个参数getter属性即是计算属性对应的方法或get方法。

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  // ... 
  
  // watcher的第二个参数,即是我们计算属性对应的方法或get方法,用于算出计算属性的值
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
    }
  }
  // 不会立即计算
  this.value = this.lazy
    ? undefined
    : this.get();
};
复制代码

那么只要调用getter方法,那么它就会触发计算属性所有依赖的data的get方法,我们看下get方法

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      // Dep.target保存的是当前正在处理的Watcher,这里其实就是computedWatcher
      if (Dep.target) {
        // 这句代码其实就是将Dep.target放入到该data属性的订阅器列表当中
        dep.depend();
        //...
      }
      return value
    },
    ...
})
复制代码

如上所述,其实就是把Dep.taget(当前的watcher)放入到该data属性的订阅器列表当中,那么这个时候,Dep.target指向哪个Watcher呢?我们看下watcher的get方法

Watcher.prototype.get = function get () {
  // 这句语句会把Dep.target执行本watcher 
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    // 调用getter,会触发上述讲的get,而get方法就会把Dep.target即本watcher放入到计算属性所依赖的data属性的订阅器列表中
    //这样依赖的data属性有改变就会调用该watcher的update方法
    value = this.getter.call(vm, vm);
  } catch (e) {
    //...
  } finally {
    //...
    popTarget(); // 将Dep.target指回上次的watcher,这里就是计算属性对应的domWatcher
    this.cleanupDeps();
  }
  return value
};
复制代码

可以看到get方法开始运行时,把Dep.target指向计算属性对应的computedWatcher,然后调用watcher的getter方法,触发这个计算属性对应的data属性的get方法,就会把Dep.target指向的watcher加入到这些依赖的data的订阅器列表当中,以此完成依赖收集。

这样当我们的计算属性依赖的data属性改变的时候,就会调用订阅器的notify方法,它会遍历订阅器列表,其中就包含了该计算属性对应的computedWatcher和domWatcher,调用computedWatcher的update方法会把computedWatcher置为dirty,调用domWathcer的update方法会触发computedGetter,它会再次调用computedWatcher的evaluate计算出最新的值交给domWatcher去更新dom。

Watcher.prototype.update = function update () {
  if (this.lazy) {
    // computed专属的watcher走这里 
    this.dirty = true;
  } else if (this.sync) {
    // run方法会调用get方法,get方法会重新计算计算属性的值
    // 但这个时候get方法不会再收集依赖了,vue会去重
    this.run();
  } else {
    queueWatcher(this);
  }
};

Watcher.prototype.run = function run () {
  if (this.active) {
    // 调用get方法,重新计算计算属性的值
    var value = this.get();
    // 值改变了、Array或Object类型watch配置了deep属性为trueif (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      var oldValue = this.value;
      this.value = value; 
      
      if (this.user) {
        // watch 监听走此处
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        // data数据改变,会触发更新函数cb,从而更新dom
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};
复制代码

vuex实现

知道了computed是如何实现的就能知道vuex如果实现依赖收集了。我们看下vuex的源码

store._vm = new Vue({
    data: {
        $$state: state
    },
    computed // 这个computed就是我们定义的computed
})
复制代码

看到这段代码大家想到什么了吗,实际上是不是依赖于computed?只不过这里的computed收集的依赖是state,当state改变的时候就会调用computed对应watcher的update方法,从而更新computed的属性值并更新dom。这也是解释了为啥state依赖需要写在computed里面。

总结

  1. 遍历computed,为每个计算属性新建一个computedWatcher对象,并将该computedWatcher的getter属性赋值为计算属性对应的方法或者get方法。(大家应该知道计算属性不但可以是一个函数,还可以是一个包含get方法和set方法的对象吧)
  2. 使用Object.defineProperty挟持计算属性的get方法,当模版获取计算属性的值的时候,触发get方法,它会调用第一步创建的computedWatcher的evaluate方法,而evaluate方法就会调用watcher的get方法
  3. computedWatcher的get方法会将Dep.target指向该computedWatcher,并调用getter方法,getter方法会触发该计算属性依赖的所有data属性的get方法,从而把Dep.target指向的computedWatcher添加到data属性的订阅器列表中。同时,computedWatcher保存了依赖的data属性的订阅器(deps属性保存)。
  4. 同时调用computedWatcher的depend方法,它会把Dep.taget指向的domWatcher放入到计算属性依赖的data属性的订阅器列表中,如此计算属性依赖的data属性改变了,就会触发domWatcher和computedWatcher的update方法,computedWatcher赋值获取计算属性的最新值,domWatcher负责更新dom。
分类:
前端
标签:
分类:
前端
标签: