computed

计算属性的初始化是发生在Vue实例初始化阶段的initState函数中,执行了

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

initComputed定义在src/core/instance/state.js中

  1. const computedWatcherOptions = { lazy: true }
  2. function initComputed (vm: Component, computed: Object) {
  3. // $flow-disable-line
  4. // watchers、vm._computedWatchers 空对象
  5. const watchers = vm._computedWatchers = Object.create(null)
  6. // computed properties are just getters during SSR
  7. const isSSR = isServerRendering()
  8. // 对computed对象做遍历
  9. for (const key in computed) {
  10. // 拿到计算属性的每一个userDef
  11. const userDef = computed[key]
  12. // 获取userDef对应的getter函数
  13. const getter = typeof userDef === 'function' ? userDef : userDef.get
  14. // 获取不到开发环境下报警告
  15. if (process.env.NODE_ENV !== 'production' && getter == null) {
  16. warn(
  17. `Getter is missing for computed property "${key}".`,
  18. vm
  19. )
  20. }
  21. // 给每个getter创建一个watcher
  22. if (!isSSR) {
  23. // create internal watcher for the computed property.
  24. watchers[key] = new Watcher(
  25. vm,
  26. getter || noop,
  27. noop,
  28. computedWatcherOptions
  29. )
  30. }
  31. // component-defined computed properties are already defined on the
  32. // component prototype. We only need to define computed properties defined
  33. // at instantiation here.
  34. // 判断key是不是vm属性
  35. if (!(key in vm)) { // 不是的话
  36. defineComputed(vm, key, userDef)
  37. } else if (process.env.NODE_ENV !== 'production') {
  38. // 否则判断计算属性key是否已经被data或者prop所占用
  39. if (key in vm.$data) {
  40. warn(`The computed property "${key}" is already defined in data.`, vm)
  41. } else if (vm.$options.props && key in vm.$options.props) {
  42. warn(`The computed property "${key}" is already defined as a prop.`, vm)
  43. } else if (vm.$options.methods && key in vm.$options.methods) {
  44. warn(`The computed property "${key}" is already defined as a method.`, vm)
  45. }
  46. }
  47. }
  48. }

watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为

  1. const computedWatcherOptions = { lazy: true }

defineComputed方法

  1. export function defineComputed (
  2. target: any,
  3. key: string,
  4. userDef: Object | Function
  5. ) {
  6. const shouldCache = !isServerRendering()
  7. if (typeof userDef === 'function') {
  8. sharedPropertyDefinition.get = shouldCache
  9. ? createComputedGetter(key)
  10. : createGetterInvoker(userDef)
  11. sharedPropertyDefinition.set = noop
  12. } else {
  13. sharedPropertyDefinition.get = userDef.get
  14. ? shouldCache && userDef.cache !== false
  15. ? createComputedGetter(key)
  16. : createGetterInvoker(userDef.get)
  17. : noop
  18. sharedPropertyDefinition.set = userDef.set || noop
  19. }
  20. if (process.env.NODE_ENV !== 'production' &&
  21. sharedPropertyDefinition.set === noop) {
  22. sharedPropertyDefinition.set = function () {
  23. warn(
  24. `Computed property "${key}" was assigned to but it has no setter.`,
  25. this
  26. )
  27. }
  28. }
  29. // 利用Object.defineProperty给计算属性对应的key值添加getter和setter
  30. Object.defineProperty(target, key, sharedPropertyDefinition)
  31. }

setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少
getter 对应的是 createComputedGetter(key) 的返回值

  1. function createComputedGetter (key) {
  2. // computedGetter 计算属性对应的getter
  3. return function computedGetter () {
  4. const watcher = this._computedWatchers && this._computedWatchers[key]
  5. if (watcher) {
  6. if (watcher.dirty) {
  7. watcher.evaluate()
  8. }
  9. if (Dep.target) {
  10. watcher.depend()
  11. }
  12. return watcher.value
  13. }
  14. }
  15. }

整个计算属性的初始化过程到这里就结束了

分析 computed watcher 的实现

计算属性是一个computed watcher,它和普通的watcher有什么区别呢
例子

  1. var vm = new Vue({
  2. data: {
  3. firstName: 'Foo',
  4. lastName: 'Bar'
  5. },
  6. computed: {
  7. fullName: function () {
  8. return this.firstName + ' ' + this.lastName
  9. }
  10. }
  11. })

初始化这个computed watcher实例时构造函数部分逻辑略有不同

  1. export default class Watcher {
  2. // ...
  3. constructor (
  4. vm: Component,
  5. expOrFn: string | Function,
  6. cb: Function,
  7. options?: ?Object,
  8. isRenderWatcher?: boolean
  9. ) {
  10. // ...
  11. // 对于computed watcher this.value值为undefined 并不会立刻求值
  12. this.value = this.lazy
  13. ? undefined
  14. : this.get()
  15. }
  16. // ...
  17. }

然后当render函数执行访问到this.fullname时就触发了计算属性的getter,它会拿到计算属性对应的watcher,然后执行watcher.depend()

  1. /**
  2. * Depend on all deps collected by this watcher.
  3. */
  4. depend () {
  5. let i = this.deps.length
  6. while (i--) {
  7. this.deps[i].depend()
  8. }
  9. }

然后再执行watcher.evaluate()去求值

  1. /**
  2. * Evaluate the value of the watcher.
  3. * This only gets called for lazy watchers.
  4. */
  5. evaluate () {
  6. this.value = this.get()
  7. this.dirty = false
  8. }

在求值过程中会执行 value = this.getter.call(vm, vm) 实际上就是执行了计算属性定义的getter函数
依赖的数据变化后的逻辑
一旦对计算属性依赖的数据修改则会触发setter过程,通知所有订阅它变化的watcher更新,执行watcher.update()方法

  1. /**
  2. * Subscriber interface.
  3. * Will be called when a dependency changes.
  4. */
  5. update () {
  6. /* istanbul ignore else */
  7. if (this.lazy) {
  8. this.dirty = true
  9. } else if (this.sync) {
  10. this.run()
  11. } else {
  12. queueWatcher(this)
  13. }
  14. }

计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化

watch

监听属性的初始化也是发生在Vue的实例初始化阶段的initState函数中,在computed初始化之后,执行了

  1. if (opts.watch && opts.watch !== nativeWatch) {
  2. initWatch(vm, opts.watch)
  3. }

initWatch定义在src/core/instance/state.js中

  1. function initWatch (vm: Component, watch: Object) {
  2. for (const key in watch) {
  3. const handler = watch[key]
  4. // Vue是支持watch的同一个key对应多个handler
  5. if (Array.isArray(handler)) {
  6. for (let i = 0; i < handler.length; i++) {
  7. createWatcher(vm, key, handler[i])
  8. }
  9. } else {
  10. createWatcher(vm, key, handler)
  11. }
  12. }
  13. }

createWatcher方法

  1. function createWatcher (
  2. vm: Component,
  3. expOrFn: string | Function,
  4. handler: any,
  5. options?: Object
  6. ) {
  7. // 对hanlder类型做判断 拿到它最终的回调函数
  8. if (isPlainObject(handler)) {
  9. options = handler
  10. handler = handler.handler
  11. }
  12. if (typeof handler === 'string') {
  13. handler = vm[handler]
  14. }
  15. // $watch是Vue原型上的方法,它是在执行stateMixin时定义的
  16. return vm.$watch(expOrFn, handler, options)
  17. }

vm.$watch定义在src/core/instance/state.js中

  1. Vue.prototype.$watch = function (
  2. expOrFn: string | Function,
  3. cb: any,
  4. options?: Object
  5. ): Function {
  6. const vm: Component = this
  7. // 判断是否是对象 $watch方法是用户可以直接调用的,它可以传递一个对象也可以传递函数
  8. if (isPlainObject(cb)) {
  9. return createWatcher(vm, expOrFn, cb, options)
  10. }
  11. options = options || {}
  12. options.user = true
  13. // 实例化一个watcher(user watcher) 因为options.user = true
  14. const watcher = new Watcher(vm, expOrFn, cb, options)
  15. if (options.immediate) {
  16. const info = `callback for immediate watcher "${watcher.expression}"`
  17. pushTarget()
  18. invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
  19. popTarget()
  20. }
  21. return function unwatchFn () {
  22. watcher.teardown()
  23. }
  24. }

通过实例化 watcher 的方式,一旦 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher

Watcher options

Watcher 支持了不同的类型,梳理一下它有哪些类型以及它们的作用
Watcher的构造函数对options做了处理

  1. // options
  2. if (options) {
  3. this.deep = !!options.deep
  4. this.user = !!options.user
  5. this.lazy = !!options.lazy
  6. this.sync = !!options.sync
  7. this.before = options.before
  8. } else {
  9. this.deep = this.user = this.lazy = this.sync = false
  10. }

watcher的四种类型

deep watcher

需要对对象做深度观测时设置这个属性为true

  1. var vm = new Vue({
  2. data() {
  3. a: {
  4. b: 1
  5. }
  6. },
  7. watch: {
  8. a: {
  9. handler(newVal) {
  10. console.log(newVal)
  11. }
  12. }
  13. }
  14. })
  15. vm.a.b = 2

这个时候是不会 log 任何数据的,因为 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了
只需要对代码做稍稍修改,就可以观测到这个变化了

  1. watch: {
  2. a: {
  3. deep: true,
  4. handler(newVal) {
  5. console.log(newVal)
  6. }
  7. }
  8. }

这样就创建了一个deep watcher了,在watcher执行get求值的过程中有判断这部分的代码

  1. // "touch" every property so they are all tracked as
  2. // dependencies for deep watching
  3. if (this.deep) {
  4. traverse(value)
  5. }

在对watch的表达式或者函数求值后会调用traverse函数
定义在src/core/observer/traverse.js中

  1. import { _Set as Set, isObject } from '../util/index'
  2. import type { SimpleSet } from '../util/index'
  3. import VNode from '../vdom/vnode'
  4. const seenObjects = new Set()
  5. /**
  6. * Recursively traverse an object to evoke all converted
  7. * getters, so that every nested property inside the object
  8. * is collected as a "deep" dependency.
  9. */
  10. export function traverse (val: any) {
  11. // 对一个对象做深层递归遍历
  12. // 遍历过程就是对一个子对象的访问,会触发它们的getter过程,这样就可以收集到依赖,也就是订阅它们变化的watcher
  13. _traverse(val, seenObjects)
  14. seenObjects.clear()
  15. }
  16. function _traverse (val: any, seen: SimpleSet) {
  17. let i, keys
  18. const isA = Array.isArray(val)
  19. if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
  20. return
  21. }
  22. if (val.__ob__) {
  23. // 把子响应式对象通过它们的depId记录到seenObjects, 避免以后重复访问
  24. const depId = val.__ob__.dep.id
  25. if (seen.has(depId)) {
  26. return
  27. }
  28. seen.add(depId)
  29. }
  30. if (isA) {
  31. i = val.length
  32. while (i--) _traverse(val[i], seen)
  33. } else {
  34. keys = Object.keys(val)
  35. i = keys.length
  36. while (i--) _traverse(val[keys[i]], seen)
  37. }
  38. }

在执行了traverse后再对watch的对象内部任何一个值做修改也会调用watcher的回调函数
如果观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置

user watcher

通过vm.$watch创建的watcher是一个user watcher,在对watcher求值以及在执行回调函数时会处理一下错误

computed watcher (lazy watcher)

计算属性

sync watcher

当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数

  1. update () {
  2. /* istanbul ignore else */
  3. if (this.lazy) {
  4. this.dirty = true
  5. } else if (this.sync) {
  6. this.run()
  7. } else {
  8. queueWatcher(this)
  9. }
  10. }

只有需要 watch 的值的变化执行 watcher 的回调函数 是一个同步过程的时候才会去设置该属性为 true

了解了 watcher 的 4 个 options,通常会在创建 user watcher 的时候配置 deep 和 sync,可以根据不同的场景做相应的配置