Computed 和 Watch

h7ml
  • vue
大约 4 分钟

Computed 和 Watch

Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。

  • 功能上:computed是计算属性,也就是依赖其它的属性计算所得出最后的值。watch是去监听一个值的变化,然后执行相对应的函数
  • 使用上:computed中的函数必须要用return返回;watch的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作,不是必须要用return
  • 性能上:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调
  • 场景上
    • computed:当一个属性受多个属性影响的时候,例子:购物车商品结算;
    • watch:当一条数据影响多条数据的时候,例子:搜索框

Watch

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

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)  // 封装
}

这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watchVue 原型上的方法,它是在执行 stateMixin 的时候定义的:

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

虽然watch内部是使用this.$watch,但是我们也是可以手动调用this.$watch来创建监听属性的,所以第二个参数cb会出现是对象的情况。接下来设置一个标记位options.usertrue,表明这是一个user-watcher

再给watch设置了immediate属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。接下来我们看看user-watcher是如何定义的:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    vm._watchers.push(this)  // 添加到当前实例的watchers内

    if(options) {
      this.deep = !!options.deep  // 是否深度监听
      this.user = !!options.user  // 是否是user-wathcer
      this.sync = !!options.sync  // 是否同步更新
    }

    this.active = true  // // 派发更新的标志位
    this.cb = cb  // 回调函数

    if (typeof expOrFn === 'function') {  // 如果expOrFn是函数
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)  // 如果是字符串对象路径形式,返回闭包函数
    }

    ...

  }
}

当是user-watcher时,Watcher内部是以上方式实例化的,通常情况下我们是使用字符串的形式创建监听属性,所以首先来看下parsePath方法是干什么的:

const bailRE = /[^\w.$]/; // 得是对象路径形式,如info.name

function parsePath(path) {
  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;
  };
}

parsePath方法最终返回一个闭包方法,此时Watcher类中的this.getter就是一个函数了,再执行this.get()方法时会将this.vm传入到闭包内,补全Watcher其他的逻辑:

总结