Vue2源码学习笔记 - 10.响应式原理一computed与watch浅析

这里我们只简单学习计算属性和侦听属性的初始化,后续响应式原理会继续深入研究学习这两个点的内容。它们形虽不同,但是本质都是依赖于 Watcher 类及与其他类模块。

Computed 计算属性的初始化

接上一节的流程,在 initState 函数的代码中我们看到有初始化计算属性的代码,代码如下:

const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed) // 初始化 computed

它把我们在实例化 Vue 应用时传入的计算属性的配置继续传入 initComputed 调用,它在文件 /src/core/instance/state.js 中,我们来看它的源码:

// 计算属性的Watcher 对象的实例化选项
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  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
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    ...

    if (!isSSR) {
      // 为每一个计算属性的 键 创建 Watcher 观察者对象
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        // Vue 实例
        vm,
        // 用户定义的求值函数 或 空函数
        getter || noop,
        // 空函数
        noop,
        // 上面定义的选项 { lazy: true }
        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)
    }
    ...
  }
}

这个函数里面大概做了两件事,一是为每个计算属性的元素创建一个 Watcher 对象,且其创建时传入了 lazy 为真的选项。二是对每一个计算属性的元素调用函数 defineComputed 定义为响应式的变量。我们跟进 defineComputed 函数看看它的代码:

// 计算属性的 Object.defineProperty 默认选项
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any, key: string, userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  // 配置Object.defineProperty 选项的 get 属性
  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
  }
  ...
  // 调用 Object.defineProperty 设置为响应式属性
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 创建计算属性的 getter 函数
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
    }
  }
}

在函数 defineComputed 中,最终就是调用 Object.defineProperty 把计算属性设置成了响应式的属性。因为我们定义的计算属性多数都是设置键和对应的求值函数,所以我们主要看 userDef 为求值函数的情况。

在这个情况下,它调用了 createComputedGetter 生成了用于 sharedPropertyDefinition.get 的 getter 函数——computedGetter,它就类似于响应式数据项的 reactiveGetter。在这个 computedGetter 中,获取该属性对应的 Watcher 对象,然后判断是否为脏状态,是则执行 Watcher.evaluate() 计算值;接着判断 Dep.target 是否为真,是则处于依赖收集状态,执行 Watcher.depend() 收集依赖。最后返回计算出的值。

Vue2源码学习笔记 - 10.响应式原理一computed与watch浅析_第1张图片

Watch 侦听属性的初始化

在初始化函数 initState 中还有侦听属性的初始化调用,代码如下:

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

它直接把侦听属性传入 initWatch 调用,我们看 initWatch 的代码:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    // 用户定义回调函数,在侦听的属性有变动时调用
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      // 侦听属性逐元素调用 createWatcher
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 最终逐元素传入调用 $watch 方法
  return vm.$watch(expOrFn, handler, options)
}

这个代码调用比较简单,在最终把侦听属性的每个元素传入 $watch 设置好侦听回调,我们继续看 $watch 方法的源代码:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    pushTarget()
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget()
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

在这个 $watch 方法中,就是把要侦听的属性和回调函数在 new Watcher 实例化时传入构造函数中,与此同时,还传入了 Watcher 的选项配置,其中 user 选项属性为真,这表示我们实例化了一个用户定义的观察者对象,这个后面在 Watcher 类详解会细说,这里我们只用知道在这个 Watcher 对象已经帮我们配置好了侦听属性,它在侦听到属性有变动时会主动调用配置的回调函数即可。

Vue2源码学习笔记 - 10.响应式原理一computed与watch浅析_第2张图片
总结:
对比数据对象的处理,计算属性和侦听属性的处理就简单多了。计算属性先经过 initComputed 给属性创建对应的 Watcher 对象,然后调用 defineComputed 定义为响应式属性,在其 computedGetter 中计算求值并收集依赖。侦听属性最终就是调用 Vue.prototype.$watch 对每一个属性都创建了一个 Watcher 对象,在属性有变动的时候会执行用户定义的回调函数。

从计算属性和侦听属性的初始化流程来看,Watcher 是实现这个功能的核心,这个类我们在接下来会研究学习,同时也会继续研究学习计算属性和侦听属性与 Watcher 的关联与配合工作的细节。

你可能感兴趣的:(Vue2源码学习笔记,vue.js,vue源码,Vue2源码)