3-2-19-Vue.js 源码阅读-watch

Vue 中的 vm.$watch 方法

vm.$watch( expOrFn, callback, [options] )

  • 参数:

    • {string | Function} expOrFn
    • {Function | Object} callback
    • {Object} [options]
      • {boolean} deep
      • {boolean} immediate
  • 返回值:{Function} unwatch

  • 用法:

    观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

    注意:在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。

使用示例

<body>
  <div id="app">
    {{ user.fullName }}
  div>

  <script src="../../dist/vue.js">script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        user: {
          firstName: '诸葛',
          lastName: '亮',
          fullName: ''
        }
      }
    });
    vm.$watch('user',
      function (newValue, oldValue) {
        this.user.fullName = newValue.firstName + ' ' + newValue.lastName
      },
      {
        immediate: true,
        deep: true
      }
    );
  script>
body>

三种类型的 Watcher 对象

  • 没有静态方法,因为 $watcher 方法中要使用 Vue 的实例
  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
    • 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • vm.$watch()
    • src/core/instance/state.js

源码阅读

三种类型的 Watcher 对象都是在 initState 这个方法中调用的。

当判断 options 中有 watch 属性的时候会调用 initWatch 这个方法初始化一个对应的用户 Watcher

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 如果 watch 的 handler 是数组
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

从上述的代码中可以看到,Vue 中的 watch 中的 handler 属性可以接收一个数组,如果是一个数组,会遍历这个数组并且给监听的属性添加多个 Watcher。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    // 把 methods 中定义的方法作为回调函数
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

从上述代码可以看到,watch 中的 handler 可以是一个字符串,这个字符串是实例中 methods 中的一个键,如果传入一个字符串,就是将 methods 中的一个方法作为侦听器的回调函数。

下面看 vm.$watch 的实现

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 获取 Vue 实例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判断如果 cb 是对象执行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 标记为用户 watcher
  options.user = true
  // 创建用户 watcher 对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判断 immediate 如果为 true
  if (options.immediate) {
    // 立即执行一个 cb 回调,并且把当前值传入
    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 属性,标记这个 Watcher 对象是一个用户 Watcher ,这个属性在创建 Watcher 的时候会用到。

/**
  * Scheduler job interface.
  * Will be called by the scheduler.
  */
run () {
  if (this.active) {
    // 调用 get 方法,初始化的时候也会调用这个方法
    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) {
        // 用户 Watcher 执行 cb,try catch 异常处理
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        // 非用户 Watcher 直接执行 cb
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

如上所述,加上 user 属性就是为了在执行 cb 的时候使用 try catch 进行异常处理。(因为用户传入的 cb 是不可控的)

在前面分析 Watcher 类的时候知道, options 中还会有一个 lazy 属性,这个 lazy 属性是在 initComputed 的时候传入 true 的。当这个属性为 true 的时候在 Watcher 的实例过程中不会立即执行 get 方法,也就是说如果是计算属性 Watcher 的时候在创建 Watcher 时先不去对其进行求值,因为计算属性的方法是直接在模板中调用的,所以计算属性对应的这个 handler 是在 render 中调用的。

你可能感兴趣的:(#,Part,3,·,Vue.js,框架源码与进阶)