vue中的watch和$watch监听的事件,执行几次?

1,140 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

面试题:$watchwatch监听的事件,执行几次?

当数据需要变化或开销较大的时候,vue提供了一个通用的方法$watch(watch)来响应数据的变化,下面分别介绍两个方法的实现原理和监听事件执行次数的分析。

一、$watch

new Vue({
  el: '#app',
  template: '<div><h3>姓名:{{person.name}},年龄:{{person.age}}</h3><button @click="change">change</button></div>',
  data: {
    person: {
      name: '水中水',
      age: 30
    }
  },
  created() {
    this.$watch(
      'person',
      function (newVal, oldVal) {
        console.log(newVal, oldVal)
      }, {
        deep: true, // 针对对象或者数组的监听
        immediate: false, // 是否立即执行,默认不立即执行
      }
    )
  },
  methods: {
    change() {
      this.person.age = this.person.age + 1;
    }
  },
})

在当前例子中定义了数据person,在created阶段通过this.$watch的方式监听了person,如果发生变化会进行新旧值的打印。

1、定义$watch

Vue 定义完成以后,在执行 stateMixin(Vue) 的时候会为 Vue.prototype 上定义 $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) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
}
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)
}

这里首先判断cb是不是对象,如果是的话执行createWatcher逻辑,将hander作为参数options, 将其中的 handler.handler作为参数 handler。然后判断handler是不是字符串,如果是的话,从当前vm实例中去寻找hander,然后再返回vm.$watch(expOrFn, handler, options)createWatcher的目的是处理非函数的cb参数,最终还是会执行$watch

如果cb是函数的话,将optionsuser设置为true,通过执行const watcher = new Watcher(vm, expOrFn, cb, options)进行Watcher的实例化。

当参数immediatetrue的时候,还会立即执行cb回调函数,当前例子为false,所以不会立即执行。

最后,返回执行watcher.teardown()unwatchFn函数,也就是说,我们可以通过this.$watch的回调函数,来取消侦听的函数。

2、实例化Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // 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()
  }

  /**
   * 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
  }
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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的过程中,需要关注传入的参数userdeeptrue,传入的immediatefalse

参数expOrFnperson,类型为string,会执行转换的逻辑this.getter = parsePath(expOrFn)

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

这里返回一个回调函数,函数在不断地通过路径访问obj的过程中,会对整条路径对应的值进行watcher的收集,例如:obj.a.b会分别对objobj.aobj.a.b访问时进行watcher的收集。当前例子objperson,那么在访问person时就会对其持有的发布者depsubs中推入当前的侦听器watcher

在执行this.get时,会执行到this.getter中返回的回调函数,实现侦听器watcher的收集。

3、监听变化

当点击change按钮对age进行增加时,会触发watcher.run()的更新逻辑。通过value = this.get()计算新值,通过const oldValue = this.value将旧值进行暂存,将this.value赋值成新值value

侦听器watcher的参数usertrue,所以会将新旧值作为参数执行this.cb.call(this.vm, value, oldValue)的逻辑,也就是当前例子中的console.log(newVal, oldVal),当然打印逻辑可以换成我们真实的业务逻辑。

4、用$watch监听两次

以上例子在created阶段修改为:

created() {
    this.$watch(
      'person',
      function (newVal, oldVal) {
        console.log(newVal, oldVal)
      }, {
        deep: true, // 针对对象或者数组的监听
        immediate: false, // 是否立即执行,默认不立即执行
      }
    )
    this.$watch(
      'person',
      function (newVal, oldVal) {
        console.log('--再次被监听--', newVal, oldVal)
      }, {
        deep: true, // 针对对象或者数组的监听
        immediate: false, // 是否立即执行,默认不立即执行
      }
    )
},

当前例子中,我们执行了两次new Watcher的逻辑,每次都会执行this.get进行值的计算,计算过程中访问person时会利用发布订阅者模式进行当前watcher的收集,当前例子中会收集两次,在数据发生变化后两次监听都会执行。

二、watch

new Vue({
  el: '#app',
  template: '<div><h3>姓名:{{person.name}},年龄:{{person.age}}</h3><button @click="change">change</button></div>',
  data() {
    return {
      person: {
        name: '水中水',
        age: 30
      }
    }
  },
  watch: {
    person: {
      handler(newVal, oldVal) {
        console.log(newVal, oldVal)
      },
      deep: true, // 针对对象或者数组的监听
      immediate: false, // 是否立即执行,默认不立即执行
    }
  },
  methods: {
    change() {
      this.person.age = this.person.age + 1;
    }
  },
})

1、initWatch

在执行new Vue的过程中,执行到initState,当满足if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); }时,执行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(vm, key, handler)
    }
  }
}

watch[key]可以是数组类型,侦听器支持数据改变的时候触发多个行为,例子中走到else逻辑,执行createWatcher(vm, key, handler)。到这里就和$watch的逻辑类似,不再赘述。

2、用watch监听两次

反例一

watch: {
    person: {
      handler(newVal, oldVal) {
        console.log(newVal, oldVal)
      },
      deep: true, // 针对对象或者数组的监听
      immediate: false, // 是否立即执行,默认不立即执行
    }
},
watch: {
    person: {
      handler(newVal, oldVal) {
        console.log('--再次被监听--', newVal, oldVal)
      },
      deep: true, // 针对对象或者数组的监听
      immediate: false, // 是否立即执行,默认不立即执行
    }
},

反例二

watch: {
    person: {
      handler(newVal, oldVal) {
        console.log(newVal, oldVal)
      },
      deep: true, // 针对对象或者数组的监听
      immediate: false, // 是否立即执行,默认不立即执行
    },
    person: {
      handler(newVal, oldVal) {
        console.log('--再次被监听--', newVal, oldVal)
      },
      deep: true, // 针对对象或者数组的监听
      immediate: false, // 是否立即执行,默认不立即执行
    }
},

以上两个例子中,因为watch或者person作为对象的键,后定义的会覆盖前面定义的,所以只会执行后面定义的监听回调。如果想实现两次监听都执行,可以采用以下写法:

watch: {
    person: [
      {
        handler(newVal, oldVal) {
          console.log(newVal, oldVal)
        },
        deep: true, // 针对对象或者数组的监听
        immediate: false, // 是否立即执行,默认不立即执行
      },
      {
        handler(newVal, oldVal) {
          console.log('--再次被监听--', newVal, oldVal)
        },
        deep: true, // 针对对象或者数组的监听
        immediate: false, // 是否立即执行,默认不立即执行
      }
    ]
},

以上写法可以实现数据变化时,监听的两个事件都执行。

总结

vuewatch$watch最终都是实例化了Watcher,并设置usertrue和其他watcher进行区分,都支持数据改变的时候触发多个行为。