响应式原理六:watcher

219 阅读3分钟

对于侦听属性 watch,监听响应式对象的变化,然后执行其回调函数。它是如何做到的呢?那么我们来探究其实现原理。

watch 初始化

在初始化 Vue 实例时,函数 initState 对侦听属性 watch 做了初始化操作,具体实现如下:

// src/core/instance/state.js

export function initState (vm: Component) {
  ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

先判断传进来的 vm 实例是否有属性 watch,如果存在,则调用函数 initWatch,否则执行后续逻辑,具体实现如下:

// src/core/instance/state.js

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(vm, key, handler)
    }
  }
}

函数接收两个参数:

  • vm:Vue 实例;
  • watchwatch 对象,包含多个侦听属性。

遍历对象 watch,通过侦听属性 key 获取到 handler,调用 Array.isArray 判断 handler 数据类型是否为数组,如果是数组的话,则对其进行遍历,最后都会调用函数 createWatcher

// src/core/instance/state.js

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]
  }
  return vm.$watch(expOrFn, handler, options)
}

函数接收 4 个参数:

  • vm:Vue 组件实例;
  • expOrFn:表达式,其数据类型为字符串或者函数;
  • handler:处理器,其数据类型为 any
  • options:可选项,其数据类型为 Object

从代码中可以看出,hanlder 数据类型可以是对象、字符串、函数等。如果是对象或者字符串的话,则需要对其进行处理;最后调用 vm 实例上函数 $watch

// src/core/instance/state.js

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()
    }
  }
}

函数接收 3 个参数:

  • expOrFn:字符串表达式或者函数;
  • cb:回调函数,即 handler
  • options:可选项。

首先,对 cb 其数据类型进行判断,如果是对象的话,则会调用函数 createWatcher 进行处理;否则,执行后续逻辑。

options 设置属性 user,其值为 true,表示 user watcher;然后初始化 wathcer 实例,核心逻辑如下:

// src/core/observer/watcher.js

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  user: boolean;
  
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
      ...
      // options
      if (options) {
        this.user = !!options.user
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      
      // 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()
    }
}

这里需要注意的是:一是设置属性 usertrue;二是 expOrFn 数据类型为字符串,需要调用函数 parsePath 对其进行处理,从而获取 getter,具体实现如下:

// src/core/util/lang.js


/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

在实例化 watcher 时,调用 this.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
}

在这里的 getter,其实是 parsePath 返回的函数,也就是说,此时会执行该函数,返回其值。如果设置属性 deeptrue,则会调用函数 traverse,具体实现如下:

// src/core/observer/traverse.js

/**
 * 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 进行依赖收集,也就是订阅它们变化的 watcher

当数据发生变化时,会通知订阅它们的 watcher 作出更新,即执行各自的回调函数。

到这里,已经分析实例化 watcher 过程,回到 vm.$watch 函数实现。

如果用户设置属性 immediate,则会立即调用回调函数,这也就解释了我们设置该属性时,在初始化 Vue 实例,用户定义的回调函数会立即被执行。

最后返回函数 unwatchFn,它会调用 watcher 方法 teardown 移除当前 watcher,具体实现如下:

// src/core/observer/watcher.js

/**
  * Remove self from all dependencies' subscriber list.
*/
teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }
}

作用是移除 wathcer,并且移除所有的依赖。

对于 watch 初始化,就分析到这里,至于其回调函数的触发时机,可参考 《响应式原理三:派发更新》。

参考链接

计算属性 VS 侦听属性