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

栏目: 编程语言 · 发布时间: 5年前

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

在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。

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

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属性为true的
    if (
      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。

以上所述就是小编给大家介绍的《带你了解vue计算属性的实现原理以及vuex的实现原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

啊哈!算法

啊哈!算法

啊哈磊 / 人民邮电出版社 / 2014-6-1 / 45.00元

这不过是一本有趣的算法书而已。和别的算法书比较,如果硬要说它有什么特点的话,那就是你能看懂它。 这是一本充满智慧和趣味的算法入门书。没有枯燥的描述,没有难懂的公式,一切以实际应用为出发点, 通过幽默的语言配以可爱的插图来讲解算法。你更像是在阅读一个个轻松的小故事或是在玩一把趣味解谜 游戏,在轻松愉悦中便掌握算法精髓,感受算法之美。 本书中涉及到的数据结构有栈、队列、链表、树......一起来看看 《啊哈!算法》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具