blog icon indicating copy to clipboard operation
blog copied to clipboard

Vue源码探秘(计算属性computed)

Open Cosen95 opened this issue 4 years ago • 0 comments

引言

计算属性(computed)是 Vue中比较强大又十分重要的功能 ,它具有 分离逻辑缓存值双向绑定 等作用或功能。

本节我就带大家一起来看下计算属性 computed部分的源码。

computed

官方文档对于计算属性的使用时机有一个很清晰的说明:如果模板内的逻辑过于复杂,那就应该考虑使用计算属性来代替。

这里引用官方的示例来说明计算属性的用法:

// App.vue

<template>
  <div id="app">
    {{ reverseName }}
  </div>
</template>
<script>
  export default {
    data() {
      return {
        name: 'forest'
      }
    },
    computed: {
      reverseName: function() {
        return this.name.split('').reverse().join();
      }
    }
  }
</script>

大致了解了computed的用法后,和之前一样,为了更清晰的了解源码的执行过程,我下面将会结合一个例子来分析源码。

从一个简单示例开始

看下这个例子:

// App.vue

<template>
  <div id="app">
    <p>{{ name }}</p>
    <button @click="handleToggleShow">toggleShow</button>
    <button @click="changeName">change</button>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        firstName: 'jack',
        lastName: 'cool',
        isShow: false
      }
    },
    computed: {
      name() {
        return this.isShow ? `${this.firstName}, ${this.lastName}` : 'please click the toggleShow button';
      }
    },
    methods: {
      handleToggleShow() {
        this.isShow = true;
      },
      changeName() {
        this.lastName = 'rose';
      }
    }
  }
</script>

在这个例子中,计算属性 name 依赖了三个响应式数据 firstNamelastNameisShow

需要注意计算属性依赖的数据必须是响应式的,否则依赖的数据发生变化并不会触发计算属性的变化。

接下来会先介绍这个例子的初始化到渲染的整个过程,然后再介绍点击 toggleShowchange 按钮时对应源码的执行过程。

计算属性的渲染过程

在前面分析组件化的时候,我们知道组件实例化前要先通过 Vue.extend 函数来创建组件构造函数:

// src/core/global-api/extend.js

Vue.extend = function (extendOptions: Object): Function {
  // ...
  if (Sub.options.computed) {
    initComputed(Sub);
  }
  // ...
};

Sub 也就是我们例子中 App 组件的构造函数,这里 Vue.extend 函数判断如果组件 options 中有 computed ,则执行 initComputed 函数,并且将 Sub 传进去:

// src/core/global-api/extend.js

function initComputed(Comp) {
  const computed = Comp.options.computed;
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key]);
  }
}

initComputed 函数拿到 computed 对象然后遍历每一个计算属性调用 defineComputed 方法,将组件原型,计算属性和对应的值传入。来看 defineComputed 函数的定义:

// src/core/instance/state.js
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);
}

首先定义了 shouldCache 表示是否需要缓存值。接着对 userDef 是函数或者对象分别处理。这里有一个 sharedPropertyDefinition ,我们来看它的定义:

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};

sharedPropertyDefinition其实就是一个属性描述符,这个在之前的章节,我们也有分析这块。

回到 defineComputed 函数。如果 userDef 是函数的话,就会定义 getter 为调用 createComputedGetter(key) 的返回值。

因为 shouldCachetrue

userDef 是对象的话,非服务端渲染并且没有指定 cachefalse 的话,getter 也是调用 createComputedGetter(key) 的返回值,setter 则为 userDef.set 或者为空。

所以 defineComputed 函数的作用就是定义 gettersetter ,并且在最后调用 Object.defineProperty 给计算属性添加 getter/setter ,当我们访问计算属性时就会触发这个 getter

对于计算属性的 setter 来说,实际上是很少用到的,除非我们在使用 computed 的时候指定了 set 函数。

无论是userDef是函数还是对象,最终都会调用createComputedGetter函数,我们来看createComputedGetter的定义:

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

可以看到,createComputedGetter 返回了一个 computedGetter 函数,也就是说计算属性的 getter 就是这个 computedGetter 函数。

我们知道访问计算属性时才会触发这个 getter,对应就是computedGetter函数被执行。所以这块逻辑在被调用时再来分析。

在创建了组件构造函数后,就会进行组件实例化。经过前面的学习,我们知道在组件实例化时会调用各种 init 函数做初始化工作,在执行 initState 的时候:

// src/core/instance/state.js

export function initState(vm: Component) {
  // ...
  const opts = vm.$options;
  if (opts.computed) initComputed(vm, opts.computed);
  // ...
}

这里opts.computed是存在的,所以会执行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) {
    // ...
  }
}

这里 initComputed 函数的 computed 参数就是我们组件中的 computed 对象。函数首先定义了两个常量,watchers 指向 vm._computedWatchers ,是一个空对象,而 isSSR 表示服务端渲染,这里为 false

接着就是遍历 computed 对象,我们分段来分析:

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

这个 getter 是我们自己编写的 computed 中的函数,也就是例子中的 name() 函数。

从源码可以看到,computed 有两种写法,一种是直接写一个函数,一种是一个对象,同时有一个 get 属性作为 getter 。如果拿不到 getter 的话就抛出警告。继续往下看:

for (const key in computed) {
  // ...
  if (!isSSR) {
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    );
  }
  // ...
}

这里判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher 实例赋值给watchers[key](对应就是vm._computedWatchers[key])。

这也说明了计算属性是通过 Watcher 来实现。

我们来看 computed Watcher 的实例化过程是怎么样的,回顾 Watcher 的定义:

// src/core/observer/watcher.js
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  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
      )
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get()
}

前面创建 computed Watcher 传入的四个参数分别是:vmgetternoopcomputedWatcherOptions{ lazy: true })。

因为这里是computed Watcher,所以 if (isRenderWatcher) 不会执行,另外 this.dirty = this.lazy = true 。接着会将 getter 也就是我们编写的 name 函数赋值给 this.getter

最后 this.value = undefined ,也就是说不会像渲染 Watcher 一样实例化时就执行get 函数调用 getter 求值。

回到 initComputed 函数的 for 循环,还有最后一段代码:

// src/core/instance/state.js
for (const key in computed) {
  // ...
  // 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);
    }
  }
}

最后一段逻辑实际上是不会执行的。if 逻辑判断的是当前计算属性有没有定义在我们的 App 组件实例上,而前面在创建 App 组件构造函数的时候,已经调用 defineComputed 函数把计算属性定义在组件原型上了,所以每个组件实例都能访问到计算属性。

else if 逻辑是判断 key 有没有定义在 dataprops 上,因为计算属性是不能和 dataprops 重名的,如果重名会抛出警告。这样整个 initComputed 函数的逻辑我们就分析完了。

接着 App 组件来到渲染阶段,我们知道渲染阶段会执行 render 函数创建 VNode ,而这个过程会访问到计算属性,这样就触发了计算属性的 getter 也就是前面提到的 computedGetter 函数:

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

computedGetter 函数首先通过 this._computedWatchers[key] 拿到前面实例化组件时创建的 computed Watcher 并赋值给 watcher 。接着有两个 if 判断,首先调用 evaluate 函数:

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */
evaluate () {
  this.value = this.get()
  this.dirty = false
}

首先调用 this.get() 将它的返回值赋值给 this.value ,回顾 get 函数:

// src/core/observer/watcher.js
/**
 * Evaluate the getter, and re-collect dependencies.
 */
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

get 函数第一步是调用 pushTargetcomputed Watcher 传入:

// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}

可以看到 computed Watcher 被 push 到 targetStack 同时将 Dep.target 置为 computed Watcher 。而 Dep.target 原来的值是渲染 Watcher ,因为正处于渲染阶段。回到 get 函数,接着就调用了 this.getter

中间具体过程之前的章节已经介绍了,这里不在赘述 。最后 get 函数会执行 popTarget()Dep.target 重新恢复为渲染 Watcher ,然后将 value 返回出去。

回到 evaluate 函数:

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

执行完get函数,将dirty置为false

回到computedGetter函数,接着往下进入另一个if判断,执行了depend函数:

// src/core/observer/watcher.js
/**
 * Depend on all deps collected by this watcher.
 */
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

这里的逻辑就是让 Dep.target 也就是渲染 Watcher 订阅了 this.dep 也就是前面实例化 computed Watcher 时候创建的 dep 实例,渲染 Watcher 就被保存到 this.depsubs 中。

在执行完 evaluatedepend 函数后,computedGetter 函数最后将 evaluate 的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。

计算属性的修改过程

在例子中,当我们点击 toggleShow 按钮时,会修改 data 中的 isShow ,就会触发 isShowsetter

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep();
  // ...
  const setter = property && property.set;
  // ...
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // ...
    },
    set: function reactiveSetter(newVal) {
      // ...
      dep.notify();
    },
  });
}

setter 的执行流程在 Vue源码探秘(派发更新) 那一节也有介绍过, 这里重点看 setter 的最后一步,也就是执行 dep.notify()派发更新:

// src/core/observer/dep.js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !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()
  }
}

notify 函数的主要逻辑就是遍历 subs 中的 Watcher 执行 update 方法。而前面也分析了 computed Watcher 订阅了计算属性依赖的 data 的变化,所以这里的 subs 存放的就是 computed Watcher ,执行了 computed Watcherupdate 方法:

// src/core/observer/watcher.js
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

渲染 Watcherupdate 方法走的是 else 逻辑执行 queueWatcher 方法。queueWatcher 函数的具体执行流程在Vue源码探秘(派发更新)那一节已经分析过了,这里就不再分析。最终页面会重新渲染。

总结

这一节我们学习了计算属性的初始化渲染过程和依赖改变时重新渲染的过程,了解到了计算属性本质上就是一个 computed watcher

下一节,我将带大家一起探秘侦听器(watch)部分的源码。

Cosen95 avatar May 19 '20 06:05 Cosen95