[Vue源码学习] watch

504 阅读3分钟

系列文章

前言

Vue通过watch选项,提供了一种更通用的方式来观察和响应Vue实例上的数据变动,那么接下来,就来看看在Vue中,是如何使用watch选项的。

watch

在初始化Vue实例的过程中,如果检测到配置中存在watch选项,就会调用initWatch方法,处理侦听属性,代码如下所示:

/* core/instance/state.js */
export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

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

可以看到,在initWatch方法中,就是遍历watch选项,继续调用createWatcher方法,做进一步的处理,代码如下所示:

/* 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)
}

可以看到,createWatcher方法就是用来规范化传入的参数,在得到handler处理函数后,就去调用$watch方法,所以在watch选项中定义的侦听属性,与手动调用$watch方法的作用是相同的。

$watch

$watch方法是在引入Vue时,添加到Vue的原型上的,代码如下所示:

/* 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) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn() {
    watcher.teardown()
  }
}

可以看到,在$watch方法中,首先在options上添加user属性,表明这是一个user watcher,然后创建Watcher实例,与之前章节中创建Watcher实例类似,但是对于自定义Watcher来说,它会传入一个在数据改变时调用的回调函数,还可以传入自定义配置选项,比如deepsync等,并且以前的渲染Watcher和计算Watcher,它们的expOrFn参数都是一个函数,而自定义Watcher除了传入函数以外,还可以是一个字符串,它会经过parsePath方法处理后,再赋值给getter属性,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    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
        )
      }
    }
    // ...
  }
}

/* core/util/lang.js */
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.getter指向了parsePath返回的匿名函数。回到Watcher的构造函数中,如果此时lazyfalse,那么就会继续调用get方法,进行依赖收集,这时,就会调用getter方法,也就是parsePath中的匿名函数,此时,匿名函数的参数obj就是当前Vue实例,segments就是通过.分割字符串后得到的片段数组,然后遍历此数组,从而获取最终的值,从这里也可以看出,对于当前还不存在的数据,进行依赖收集时是不会报错的。

执行完watcher.get方法后,将待侦听的属性对应的值保存在watcher.value中,并且此表达式与自定义Watcher之间,已经成功构建了联系。回到$watch方法中,最后会返回一个unwatchFn方法,用来取消观察,调用此方法时,会调用watcher.teardown方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  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
    }
  }
}

可以看到,在teardown方法中,首先对此Watcher所依赖的数据,通过调用dep.removeSub方法进行取消依赖,然后将此Watcheractive选项置为false,从而取消观察,这样一来,当侦听的属性发生变化时,就不会触发该Watcher的更新了。

接下来,就来看看数据发生变化时,Vue是如何处理的。

update

当观察的数据发生变化时,就会通知此Watcher调用它的update方法进行更新,对于非syncWatcher来说,同样也会将Watcher实例推入queue中,然后在下一帧中执行watcher.run方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run() {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

可以看到,在watcher.run方法中,首先还是会调用watcher.get方法,重新收集依赖,得到更新之后的值,如果和上一次的value不相同时,就会调用watcher.cb方法,也就是调用$watch时传入的handle处理函数,同时将新旧value作为参数传入,所以我们定义的回调函数才可以执行。这就是自定义Watcher更新时的逻辑。

除了默认操作外,自定义Watcher还可以接收一些额外的选项,那么接下来,就来看看这些选项是如何运作的。

immediate

在调用$watch创建自定义Watcher后,如果immediate选项为true,就会立即调用一次cb回调函数,代码如下所示:

/* core/instance/state.js */
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // ...
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // ...
}

deep

在调用watcher.get进行依赖收集时,如果deep选项为true,就会调用traverse方法,进行深度依赖,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  get() {
    // ...
    if (this.deep) {
      traverse(value)
    }
    // ...
  }
}

/* core/observer/traverse.js */
const seenObjects = new Set()

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

在调用traverse方法时,此时的Dep.target还是指向当前的Watcher实例,然后调用_traverse方法,深度递归的对碰触到的每个数据都进行依赖收集,并且还使用seenObjects避免循环依赖,最终,此Watcher会依赖所有碰触到的数据,任意层次的数据发生变化时,都会通知此Watcher进行更新。

sync

Watcher需要进行更新时,如果sync选项为true,不会将此Watcher添加到queue中,而是直接执行watcher.run方法,进行更新操作,代码如下所示:

export default class Watcher {
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

总结

Vue中,可以使用watch选项或$watch方法,对属性进行侦听,当这些数据发生变化时,会执行我们传入的回调函数,并且自定义Watcher还可以支持多种配置选项,从而实现不同的功能。