Vue源码解析系列——响应式原理篇:computed

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

写在前面

computed的内部原理较为复杂,需要对DepWatcher类需要有较深的理解,如果还有同学不理解DepWatcher类可以去看我之前的文章:理解Dep类和Watcher类

这里我简单提两句。试想:一个data数据会不会被多个computed所依赖?那么一个computed会不会同时依赖多个data属性?

对后台数据库表关系设计有了解的同学应该知道表与表之间有一种关系:多对多的关系。多对多的关系是数据库表设计的精髓所在,在Vue中,其实DepWatcher类就是一种多对多的关系。

Dep实例上有个属性,this.subs,subs其实就是英文:订阅者subscriber 的缩写,在DepWatcher的关系中,订阅者为Watcher,发布者为Dep,所以Dep实例上的subs存储的就是多个Watcher实例。Watcher实例上有个属性this.deps,顾名思义,就是存储多个依赖,也就是Dep实例的。

computed

面试官经常会问:“请问computed和watch的区别是什么?”
你可能会答:“computed和watch非常类似,都是在data数据改变的过程中可以触发对应的方法。而computed不同的是,首先computed有一个懒加载机制,在初始化后如果不获取他的值,是不会触发计算的。其次computed有一个缓存机制,当data数据没有发生改变时,computed不会重新计算,而是拿出上一次计算好的值;只有当computed依赖的data发生改变时,computed才会重新计算。”

其实这就是computed的主要原理了,但是这里有几个关键词我们需要标注下:懒加载缓存依赖
带着这几个关键词,去阅读源码会更加清晰。

computed初始化

computed初始化在_init中:initState(vm)initState中又有这样一段代码:

 if (opts.computed) initComputed(vm, opts.computed);

调用initComputed,传入vm.options.computed
进入initComputed

initComputed

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];
    //定义getter,如果computed是个函数,getter就是函数本身,如果computed是个对象,getter就是对象的get属性
    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.
      //非服务器渲染环境下实例化watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions //lazy watcher 此时不会进行求值
      );
    }

    // 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)) {
      //如果compouted中的变量在vm中没有定义过,就调用 defineComputed
      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
        );
      }
    }
  }
}

initComputed主要做了三件事:

  1. 遍历vm.options.computed对象,挨个创建对应的Watcher实例并全部挂载到vm._computedWatchers
  2. 对每一个computed计算属性都用defineComputed方法进行处理

在创建computed watcher时,传入getter,当计算属性为一个对象时,getter是对象中的get方法;当计算属性为函数时,getter就是计算属性函数本身。接下来传入了一个配置选项computedWatcherOptions,在initComputed方法定义的上面可以找到computedWatcherOptions :

const computedWatcherOptions = { lazy: true };

接着我们进入Watcher的构造器看看做了什么操作。

Watcher.constructor

this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        process.env.NODE_ENV !== "production" &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    //computed不会立刻求值
    this.value = this.lazy ? undefined : this.get();

在这时,这边的关键代码只有两行:

 this.dirty = this.lazy; // for lazy watchers
 ...
 this.value = this.lazy ? undefined : this.get();

可以看到将lazy赋值给了dirty,至于这个dirty有什么用我们接下来会介绍。
之后不执行this.get(),所以此时的value是空。
在这里就印证了我们刚刚在上面提到过的:computed有一个懒加载机制,在初始化后如果不获取他的值,是不会触发计算的。这边在初始化果然不会立即取值。
好了,我们回到initComputed继续向下解读defineComputed方法。

defineComputed

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);
}

首先判断是不是服务端渲染,我们这边以客户端渲染为例,如果不是服务端渲染的话就定义shouldCachetrue
如果computed是以函数形式为定义的话就设置sharedPropertyDefinition.getcreateComputedGetter(key)执行后的结果。
如果是以对象形式定义的话,且用户设置cachetrue,也是设置sharedPropertyDefinition.getcreateComputedGetter(key)执行后的结果。

sharedPropertyDefinition的定义:

const sharedPropertyDefinition = {
 enumerable: true,
 configurable: true,
 get: noop,
 set: noop,
};

其实就是一个对象属性描述符。
defineComputed最后,使用Object.definePropertycomputed直接挂在到了Vue实例vm上,这就是为什么我们平时可以直接使用this.xxx来访问computed中的属性了。在以this.xxx访问computed属性的过程中,就会触发刚刚定义的get,也就是调用了createComputedGetter(key)

接下来,我们来解析createComputedGetter(key)做了什么。

createComputedGetter()

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      //首次调用时:数据为 脏 状态(具体可见watcher的构造函数),所以需要立即求值
      //非首次调用时:需判断数据是否为 脏 状态
      //如果为 脏 状态,重新进行求值
      //如果为 干净 状态,不进行重新计算
      if (watcher.dirty) {
        //Dep.target = computedWatcher
        watcher.evaluate();
        //Dep.target = renderWatcher
      }

      //render watcher被添加到computed watcher的依赖中
      if (Dep.target) {
        //Dep.target = renderWatcher
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

别看createComputedGetter就这么短短几行代码,这几行可是computed的全部精髓所在,在这里要佩服一下尤大,实在是太厉害了。

首先这是一个闭包结构,将key放入了闭包中,这样调用多次computedget都不需要去关心key的获取。

进入内部函数:
首先根据key值获取了之前挂载在Vue实例vm上的所有computed watcher
如果computed watcher存在,就判断computed watcher实例属性dirty,这边我们刚刚在构造函数中看到了dirtytrue,所以会调用watcher的实例方法evaluate
接下来又判断Dep.target是否存在,如果存在就调用watcher的实例方法depend
最后返回了watcher实例属性value,也就是computed计算后的结果。

这边有两个方法我们需要关注:watcher.evalute()watcher.depend()

watcher.evalute()

computedget过程中,如果computed依赖已经发生过变化时,就会先调用watcher.evalute获取最新的值。

 evaluate() {
    this.value = this.get();
    //将数据设置为 干净 状态,表示缓存已更新
    this.dirty = false;
 }

这里的逻辑很简单,调用实例方法get()获取最新的值,之后再设置dirty状态为false(缓存值)

watcher.depend()

 depend() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
      //dep.addSub(Dep.target)
    }
  }

别看这里就三行代码,这里的逻辑可是是整个computed最难的部分。

  • 从后往前遍历实例上的deps属性
  • 调用各个deps中的depend方法

要坐稳了,开始上天了:

1.this.deps中收集的是和这个computed有关联的所有data数据的Dep依赖收集器。
2. 调用这些data的依赖收集器的方法depend
3.dep.depend中会调用当前的Dep.target上的addDep。(Dep.target就是当前正在运行的Watcher实例,此处为页面的render watcher
4. 调用Dep.target.addDep,将对应的data的依赖收集器放入自身的deps属性内,然后再调用dep.addSub(this)data的依赖收集器收集自身。

我的内心:???
在这里插入图片描述

好了好了,我来简化一下流程,让这段逻辑能更加容易理解一些:
假设现在页面上有个computed属性,当页面渲染时:

  • 此时的Dep.target是页面的render watcher
  • 调用computed属性上的get方法,由于首次渲染,computed还没有缓存过值,
    所以直接调用watcher.evaluate()
  • watcher.evaluate()会调用watcher.get(),在watcher.get()中会先调用一次pushTarget改变当前的Dep.target,此时的Dep.target是这个computed wathcer。计算完毕后又会调用popTarget,归还原先的Dep.target,此时Dep.target是原来的render watcher
  • 调用watcher.depend
  • 查找computed watcher的依赖列表,调用这个computed属性所依赖的所有data属性的依赖收集器中的depend方法,将此时的Dep.target收集到这些data属性的依赖收集器内。此时data的依赖收集器中的依赖一共有两种类型:render watchercomputed wathcer
  • 返回 computed 中计算后的值,渲染值页面。

此时,用户的交互改变了data属性中的值:

  • 触发data中的setter函数
  • 通知data依赖收集器中的所有依赖于这个data的依赖进行更改
  • computed watcher触发update更改值
  • render watcher重新渲染页面上computed的显示值

总结

虽然页面中需要用到computed属性。但是!页面并不直接依赖于computed!页面还是依赖于data!因为只有data才会触发computed的更改,只有data的更改才会引起页面的重新渲染!computed只是起到了一个牵线作用,将页面与data连接了起来!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值