vue2源码记录6

80 阅读4分钟

computed vs. watch

通过源码学习解决何时用computed,何时用watch的问题。

computed

计算属性初始化:

// 定义 computed watcher 标志,lazy 属性为 true
const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  // 定义一个 watchers 为空对象
  // 并且为 vm 实例上也定义 _computedWatchers 为空对象,用于存储 计算watcher
  // 这使得 watchers 和 vm._computedWatchers 指向同一个对象
  // 也就是说,修改 watchers 和 vm._computedWatchers 的任意一个都会对另外一个造成同样的影响
  const watchers = vm._computedWatchers = Object.create(null)

  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  // 遍历 computed 中的每一个属性值,为每一个属性值实例化一个计算 watcher
  for (const key in computed) {
    // 获取 key 的值,也就是每一个 computed
    const userDef = computed[key]

    // 用于传给 new Watcher 作为第二个参数
    // computed 可以是函数形式,也可以是对象形式,对象形式的则取里面的 get
    // computed: { getName(){} } | computed: { getPrice: { get(){}, set() {} } }
    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.
      // 为每一个 computed 添加上 计算watcher;lazy 为 true 的 watcher 代表 计算watcher
      // 在 new watcher 里面会执行 this.dirty = this.lazy; 所以刚开始 dirty 就是 true
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions  // const computedWatcherOptions = { lazy: true }
      )
    }

    // 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)) {
      // 将 computed 属性代理到 vm 上,使得可以直接 vm.xxx 的方式访问 computed 的属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 在非生产环境会判重,computed 的属性不能和 data、props 中的属性重复
      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)
      }
    }
  }
}

defineComputed的定义:

// 将 computed 的 key 代理到 vm 实例上
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  // shouldCache 用来判断是客户还是服务端渲染,客户端需要缓存
  const shouldCache = !isServerRendering()

  // 如果是客户端,使用 createComputedGetter 创建 getter
  // 如果是服务端,使用 createGetterInvoker 创建 getter
  // 两者有很大的不同,服务端渲染不会对计算属性缓存,而是直接求值
  if (typeof userDef === 'function') {
    // computed 是函数形式
    sharedPropertyDefinition.get = shouldCache ?
      createComputedGetter(key) :
      createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 如果 computed 是对象形式
    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
      )
    }
  }

  // 拦截对 computed 的 key 访问,代理到 vm 上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

通过Object.defineProperty设置getter和setter,并将computed的属性代理到vm上,可以直接访问。

通过createComputedGetter得到客户端的getter函数:

// 用于创建客户端的 conputed 的 getter
// 由于 computed 被代理了,所以当访问到 computed 的时候,会触发这个 getter
function createComputedGetter(key) {
  // 返回一个函数 computedGetter 作为劫持 computed 的 getter 函数
  return function computedGetter() {
    // 每次读取到 computer 触发 getter 时都先获取 key 对应的 watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // dirty 是标志是否已经执行过计算结果;dirty=true,代表有脏数据,需要重新计算
      // dirty 初始值是 true(在 new Watcher 时确定),所以 computed 首次会进行计算,与 watch 略有差别
      // 如果执行过并且依赖数据没有变化则不会执行 watcher.evaluate 重复计算,这也是缓存的原理
      // 在 watcher.evaluate 中,会先调用 watcher.get 进行求值,然后将 dirty 置为 false
      // 在 watcher.get 进行求值的时候,访问到 data 的依赖数据,触发 data 数据的 get,收集 计算watcher
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        // 进行依赖收集
        // 注意,这里收集的是 渲染watcer,而不是 计算watcher
        watcher.depend()
      }

      // 返回结果
      return watcher.value
    }
  }
}

触发渲染watcher:

  update() {
    /* istanbul ignore else */
    // lazy 为 true 代表是 computed
    if (this.lazy) {
      // 如果是 计算watcher,则将 dirty 置为 true
      // 当页面渲染对计算属性取值时,触发 computed 的读取拦截 getter
      // 然后执行 watcher.evaluate 重新计算取值
      this.dirty = true;
    } 
    }

watch

watch的初始化:

function initWatch(vm: Component, watch: Object) {
  // 遍历 watch 对象
  for (const key in watch) {
    // 获取 handler = watch[key]
    const handler = watch[key]
    // handler可以是数组的形式,执行多个回调
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

vue支持一个key对应多个handler(没用过)。

createWatcher的实现:


function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options ? : Object
) {
  // 如果 handler(watch[key]) 是一个对象,那么获取其中的 handler 方法
  // watch: {
  //   a: {
  //     handler(newName, oldName) {
  //       console.log('obj.a changed');
  //     },
  //     immediate: true, // 立即执行一次 handler
  //     // deep: true
  //   }
  // }
  if (isPlainObject(handler)) {
    // 如果是对象,那么 options 就是 watch[key]
    options = handler
    // handler 是 watch[key].handler
    handler = handler.handler
  }

  // watch 也可以是字符串形式
  // methods: {
  //   userNameChange() {}
  // },
  // watch: {
  //   userName: 'userNameChange'
  // }
  // 如果 handler(watch[key]) 是字符串类型
  if (typeof handler === 'string') {
    // 找到 vm 实例上的 handler
    handler = vm[handler]
  }

  // handler(watch[key]) 不是对象也不是字符串,那么不需要处理 handler,直接执行 vm.$watch
  // 例如:watch: { a(newName, oldName) {} }
  /**
   * expOrFn: 就是每一个 watch 的名字(key 值)
   * handler: watch[key]
   * options: 如果是对象形式,options 有值,不是,可能是 undefined
   */
  return vm.$watch(expOrFn, handler, options)
}

watch最终会调用vm.$watch

 Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options ? : Object
  ): Function {
    const vm: Component = this

    // 先判断一下 handler 会不会是对象,是对象,继续调用 createWatcher 处理
    // 这里是因为有这种情况:this.$watch('msg', { handler: () => {} }) 直接调用
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }

    // 如果 options 是 undefined,将 options 赋值为空对象 {}
    options = options || {}

    // options.user 这个是用户定义 watcher 的标志
    options.user = true

    // 创建一个user watcher
    // 在实例化 user watcher 的时候会执行一次 getter 求值,这时,user watcher 会作为依赖被数据所收集
    const watcher = new Watcher(vm, expOrFn, cb, options)

    // 如果有 immediate,立即执行回调函数 handler
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }

    // 返回 unwatch 函数,用于取消 watch 监听
    return function unwatchFn() {
      watcher.teardown()
    }
  }
}

深度依赖-deep

如果设置了deep:true,在get时会执行traverse。

const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

在深层递归遍历时,对每个子对象进行访问,触发它们的getter过程。具体实现中,会把对象的dep.id记录到set数据结构中,避免重复访问。deep的设置可以在深层数据变化时触发回调,但是深层递归会有一定的性能开销(Vue3.5中deep可以设为整数,用来指定监听的深度)。

computed vs. watch

  • computed支持缓存,watch不支持
  • watch支持异步,computed不支持
  • computed默认首次监听,wacth默认首次不执行回调,可以更改immediate属性
  • computed直接使用整个对象不会深度监听,需要调用到对象的具体属性(a.b.c),watch可以设置deep属性实现深度细致监听
  • computed适合属性依赖其他属性计算而来,watch监听的值必须是data声明过的值或父组件传递的props中的数据