浅析watch使用方法及从内部实现解析watch的工作原理

一、watch 使用方法

1、handler方法和immediate属性

  这里 watch 的一个特点是,最初绑定的时候是不会执行的,要等到 firstName 改变时才执行监听计算。那我们想要一开始就让他最初绑定的时候就执行改怎么办呢?我们需要修改一下我们的 watch 写法,修改过后的 watch 代码如下:

watch: {
  firstName: {
    handler(newName, oldName) {
      this.fullName = newName + ' ' + this.lastName;
    },
    // 代表在wacth里声明了firstName这个方法之后立即先去执行handler方法
    immediate: true
  }

  注意到handler了吗,我们给 firstName 绑定了一个handler方法,之前我们写的 watch 方法其实默认写的就是这个handler,Vue.js会去处理这个逻辑,最终编译出来其实就是这个handler。

  而immediate:true代表如果在 wacth 里声明了 firstName 之后,就会立即先去执行里面的handler方法,如果为 false 就跟我们以前的效果一样,不会在绑定的时候就执行。

2、deep属性

  watch 里面还有一个属性 deep,默认值是 false,代表是否深度监听。deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj里面任何一个属性都会触发这个监听器里的 handler

3、注销watch

  为什么要注销 watch?因为我们的组件是经常要被销毁的,比如我们跳一个路由,从一个页面跳到另外一个页面,那么原来的页面的 watch 其实就没用了,这时候我们应该注销掉原来页面的 watch 的,不然的话可能会导致内置溢出。好在我们平时 watch 都是写在组件的选项中的,他会随着组件的销毁而销毁。

二、从内部实现解析watch的工作原理

  watch 的几个特点:

(1)watch监听优化: 监听某个对象时,对象的任何属性改变都会触发变动, 这样比较耗性能, 如果明确知道只需监听某一属性,可以使用字符串的形式监听,如'element.data'
(2)watch有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,可添加immediate属性
(3)普通watch方法不能监听对象内部属性的变化,可以添加deep属性深度监听

  那么watch内部是如何实现监听的呢?要知道watch的工作原理, 需要了解三个地方:

1、监听数据改变时,watch 如何工作的?

  Vue会把数据设置响应式,即设置他的 get 和 set。当数据被读取,get被触发,然后收集到读取他的东西,保存到依赖收集器。当数据被改变,set被触发,然后通知曾经读取他的东西进行更新。

  watch 在一开始初始化的时候,会读取一遍监听的数据的值,于是那个数据就收集到 watch 的 watcher 了 。然后你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中 。当数据改变时,通知 watch 的 watcher 进行更新,于是你设置的 handler 就被调用了。

2、设置 immediate 时,watch 如何工作的?

  设置了 immediate 时,就不需要在数据改变的时候才会触发。 而是在初始化 watch 时,在读取监听数据的值之后,便立即调用一遍你设置的监听回调,然后传入刚读取的值。

3、设置了 deep 时,watch 如何工作的?

  watch 有一个 deep 选项,是用来深度监听的。什么是深度监听呢?就是当你监听的属性的值是一个对象的时候,如果你没有设置深度监听,当对象内部变化时,你监听的回调是不会被触发的。

  先看源码,看划线的2处,可以看到, 当存在deep属性时,会执行_traverse方法。 简单来讲,就是递归收集对象或数组的子属性值。

三、源码解读

  我们常用于设置的 watch,其实它只是this.$watch这个API的一种封装:

export default {
  watch: {
    name(newName) {...}
  }
}
// 实际
export default {
  created() {
    this.$watch('name', newName => {...})
  }
}

1、监听属性初始化:首先来看下初始化watch属性时都做了什么

function initState(vm) {  // 初始化所有状态时
  vm._watchers = []  // 当前实例watcher集合
  const opts = vm.$options  // 合并后的属性
  ... // 其他状态初始化
  if(opts.watch) {  // 如果有定义watch属性
    initWatch(vm, opts.watch)  // 执行初始化方法
  }
}

function initWatch (vm, watch) {  // 初始化方法
  for (const key in watch) {  // 遍历watch内多个监听属性
    const handler = watch[key]  // 每一个监听属性的值
    if (Array.isArray(handler)) {  // 如果该项的值为数组
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])  // 将每一项使用watcher包装
      }
    } else {
      createWatcher(vm, key, handler) // 不是数组直接使用watcher
    }
  }
}

function createWatcher (vm, expOrFn, handler, options) {
  if (isPlainObject(handler)) { // 如果是对象,参数移位
    options = handler  
    handler = handler.handler
  }
  if (typeof handler === 'string') {  // 如果是字符串,表示为方法名
    handler = vm[handler]  // 获取methods内的方法
  }
  return vm.$watch(expOrFn, handler, options)  // 封装
}

  以上对监听属性的多种不同的使用方式,都做了处理,可以看到最后是调用了vm.$watch方法

2、监听属性的实现原理:来看下$watch的内部实现:

Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
  const vm = this
  if (isPlainObject(cb)) {  // 若cb是对象,当手动创建监听属性时
    return createWatcher(vm, expOrFn, cb, options)
  }
  options.user = true  // user-watcher的标志位,传入Watcher类中
  const watcher = new Watcher(vm, expOrFn, cb, options)  // 实例化user-watcher
  if (options.immediate) {  // 立即执行
    cb.call(vm, watcher.value)  // 以当前值立即执行一次回调函数
  }  // watcher.value为实例化后返回的值
  return function unwatchFn () {  // 返回一个函数,执行取消监听
    watcher.teardown()
  }
}

export default {
  data() {
    return {
      name: 'cc'
    }  
  },
  created() {
    this.unwatch = this.$watch('name', newName => {...})
    this.unwatch()  // 取消监听
  }
}

  注意上面的 2 个地方:options.immediate 和 return unwatchFn 函数执行取消监听

  虽然watch内部是使用this.$watch,但是我们也是可以手动调用this.$watch来创建监听属性的,所以第二个参数cb会出现是对象的情况。接下来设置一个标记位options.usertrue,表明这是一个user-watcher再给watch设置了immediate属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。

3、监听属性的deep深度监听原理

  在 Watcher 类的 get方法内,如果有deep属性,则执行traverse方法

4、小结

(1)watchthis.$watch的实现是一致的,其原理就是为需要观察的数据创建并收集user-watcher,当数据改变时通知到user-watcher将新值和旧值传递给用户自己定义的回调函数

  为什么watch的回调内可以得到新值和旧值的原理,因为cb.call(this.vm, value, oldValue)这句代码的原因,内部将新值和旧值传给了回调函数

(2)定义watch时会被使用到的三个参数:syncimmediatedeep。简单说明它们的实现原理就是:sync是不将watcher加入到nextTick队列而同步的更新、immediate是立即以得到的值执行一次回调函数、deep是递归的对它的子值进行依赖收集。

posted @ 2018-05-30 23:28  古兰精  阅读(7350)  评论(3编辑  收藏  举报