美文网首页前端Vue专辑
vue2.0源码解读 - 监听属性 watch

vue2.0源码解读 - 监听属性 watch

作者: 小马嗒 | 来源:发表于2019-11-03 17:45 被阅读0次

    侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:

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

    这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher

    function createWatcher (
      vm: Component,
      keyOrFn: string | Function,
      handler: any,
      options?: Object
    ) {
      if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
      }
      if (typeof handler === 'string') {
        handler = vm[handler]
      }
      return vm.$watch(keyOrFn, handler, options)
    }
    

    这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watchVue 原型上的方法,它是在执行 stateMixin (),目录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) {
          cb.call(vm, watcher.value)
        }
        return function unwatchFn () {
          watcher.teardown()
        }
      }
    

    也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcherrun方法,执行回调函数 cb,并且如果我们设置了immediatetrue,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
    所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
    Watcher 的构造函数对 options 做的了处理,代码如下:

        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
    

    所以 watcher 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。

    deep watcher

    通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

    var vm = new Vue({
      data() {
        a: {
          b: 1
        }
      },
      watch: {
        a: {
          handler(newVal) {
            console.log(newVal)
          }
        }
      }
    })
    vm.a.b = 2
    

    这个时候是不会 log 任何数据的,因为我们是 watcha 对象,只触发了 agetter,并没有触发 a.bgetter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
    而我们只需要对代码做稍稍修改,就可以观测到这个变化了

    watch: {
      a: {
        deep: true,
        handler(newVal) {
          console.log(newVal)
        }
      }
    }
    

    这样就创建了一个 deep watcher 了,在 watcher 执行 get 求值的过程中有一段逻辑:

    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
       traverse(value) // 触发它所有子项的 `getter`
    }
    

    在对 watch 的表达式或者函数求值后,会调用 traverse 函数,它的定义在 src/core/observer/traverse.js 中:

    /**
     * Recursively traverse an object to evoke all converted 递归遍历一个对象以调用所有转换的
     * getters, so that every nested property inside the object getter,以便对象中的每个嵌套属性
     * is collected as a "deep" dependency. 作为“深层”依赖关系收集
     */
    const seenObjects = new Set()
    function traverse (val: any) {
      seenObjects.clear()
      _traverse(val, seenObjects)
    }
    
    function _traverse (val: any, seen: ISet) {
      let i, keys
      const isA = Array.isArray(val)
      if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
        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 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。
    那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。
    deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deeptrue,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。

    user watcher

    前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:

    get() {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    },
    set() {
      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)
      }
    }
    

    handleErrorVue 中是一个错误捕获并且暴露给用户的一个利器。

    computed watcher

    computed watcher 几乎就是为计算属性量身定制的,之前文章中有对它做了详细的分析,这里不再赘述了,详细点这里

    sync watcher

    在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

      /**
       * Subscriber interface.
       * Will be called when a dependency changes. // 
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    

    只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true

    相关文章

      网友评论

        本文标题:vue2.0源码解读 - 监听属性 watch

        本文链接:https://www.haomeiwen.com/subject/tsflbctx.html