Vue computed 实现原理

一、Computed 是什么?

使用例子:

<div>{{ fullName }}</div>
复制代码
new Vue({
    data: {
        firstName: "Xiao",
        lastName: "Ming"
    },
    computed: {
        fullName: function() {
            return this.firstName + " " + this.lastName;
        }
    }
})
复制代码

Vue 中我们不需要在 template 里面直接计算{{ this.firstName + ” ” + this.lastName }},因为在模版中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

二、原理分析

在分析 computed 源码之前我们得先对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。

WechatIMG12.jpeg

注:当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将会遍历此对象的所有属性,并使用 object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

Vue 响应系统,其核心有三点:observe、watcher、dep:

  • observe:遍历 data 中的属性,使用 Object.defineProperty 的get/set 方法对其进行数据挟持;
  • dep:每个属性拥有自己的消息订阅器,用于存放所有订阅了该属性的观察者对象;
  • watcher:观察者对象,通过 dep 实现对响应属性的监听,监听结果后,主动出发自己的回调进行响应。

对 Vue 的响应式有一个初步了解后,我们再来分析计算属性。首先我们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中完成的

export function initState(vm: Component) {
    vm._watcher = [];
    const opts = vm.$options;
    if(opts.props) initProps(vm, opts.props);
    if(opts.methods) initMethods(vm, opts.methods);
    if(opts.data) {
        initData(vm);
    } else {
        observe((vm._data = {}), true /* asRootData */);
    }
    // computed 初始化
    if(opts.computed) initComputed(vm, opts.computed);
    if(opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}
复制代码

调用了 initComputed 函数(其前后也分别初始化了 initData 和 initWatch)并传入两个参数 vm 实例 和 开发者定义的 computed 选项:

const computedWatcherOptions = { computed: true };
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];
    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.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // 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)) {
      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
        );
      }
    }
  }
}
复制代码

从这段代码开始我们观察这几部分:

获取计算属性点定义 userDef 和 getter 求值函数

const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
复制代码

定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 set 和 get 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

计算属性点观察者 watcher 和 消息订阅器 dep

watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
复制代码

这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性点观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了4个参数:vm 实例、getter 求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 { computed: true } 项,表明这是一个计算属性而不是非计算属性的观察者),下面我们来看一下 Watcher 构造函数的定义:

class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    if (options) {
      this.computed = !!options.computed;
    }

    if (this.computed) {
      this.value = undefined;
      this.dep = new Dep();
    } else {
      this.value = this.get();
    }
  }

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

  update() {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true;
      } else {
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }

  evaluate() {
    if (this.dirty) {
      this.value = this.get();
      this.dirty = false;
    }
    return this.value;
  }

  depend() {
    if (this.dep && Dep.target) {
      this.dep.depend();
    }
  }
}
复制代码

为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码。观察 Watcher 的 constructor,结合刚才讲到的 new Watcher 传入的第四个参数 { computed: true } 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

export default class Dep {
  static target: ?Watcher;
  subs: Array<Watcher>;

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

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

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

  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
Dep.target = null;
复制代码

Dep 同样精简了部分代码,我们观察 Watcher 和 Dep 的关系,用一句话总结

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

defineComputed 定义计算属性

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.prop) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm);
    }
}
复制代码

因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm 实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。然后继续找到 defineComputed 定义处:

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering();
  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;
  }
  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);
}
复制代码

在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符 sharedPropertyDefinition,初始化为:

const sharedPropertyDefinition = {
   enumerable: true,
   configurable: true,
   get: noop,
   set: noop
};
复制代码

随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      watcher.depend();
      return watcher.evaluate();
    }
  },
  set: userDef.set || noop,
};
复制代码

当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 watcher.depend() 收集依赖和 watcher.evaluate() 计算求值。

分析完所有步骤,我们最后再来总结下整个过程:

  • 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observe 遍历 data 中每个属性设置 get/set 数据拦截。
  • 初始化 computed 会调用 initComputed 函数。
  • 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher):
    • 调用计算属性时会触发其 Object.defineProperty 的 get 访问器函数。
    • 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher。
    • 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋值给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、prop 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中的属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
  • 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 watcher 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享