Computed 和 Watch

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

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

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

Watch

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

1function initState(vm) {  // 初始化所有状态时
2  vm._watchers = []  // 当前实例watcher集合
3  const opts = vm.$options  // 合并后的属性
4
5  ... // 其他状态初始化
6
7  if(opts.watch) {  // 如果有定义watch属性
8    initWatch(vm, opts.watch)  // 执行初始化方法
9  }
10}
11
12---------------------------------------------------------
13
14function initWatch (vm, watch) {      // 初始化方法
15  for (const key in watch) {          // 遍历watch内多个监听属性
16    const handler = watch[key]        // 每一个监听属性的值
17    if (Array.isArray(handler)) {     // 如果该项的值为数组
18      for (let i = 0; i < handler.length; i++) {
19        createWatcher(vm, key, handler[i])  // 将每一项使用watcher包装
20      }
21    } else {
22      createWatcher(vm, key, handler) // 不是数组直接使用watcher
23    }
24  }
25}
26
27---------------------------------------------------------
28
29function createWatcher (vm, expOrFn, handler, options) {
30  if (isPlainObject(handler)) { // 如果是对象,参数移位
31    options = handler
32    handler = handler.handler
33  }
34  if (typeof handler === 'string') {  // 如果是字符串,表示为方法名
35    handler = vm[handler]  // 获取methods内的方法
36  }
37  return vm.$watch(expOrFn, handler, options)  // 封装
38}

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

1Vue.prototype.$watch = function (expOrFn, cb, options = {}) {
2  const vm = this
3  if (isPlainObject(cb)) {
4    // 如果cb是对象,当手动创建监听属性时
5    return createWatcher(vm, expOrFn, cb, options)
6  }
7
8  options.user = true // user-watcher的标志位,传入Watcher类中
9  const watcher = new Watcher(vm, expOrFn, cb, options) // 实例化user-watcher
10
11  if (options.immediate) {
12    // 立即执行
13    cb.call(vm, watcher.value) // 以当前值立即执行一次回调函数
14  } // watcher.value为实例化后返回的值
15
16  return function unwatchFn() {
17    // 返回一个函数,执行取消监听
18    watcher.teardown()
19  }
20}

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

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

1class Watcher {
2  constructor(vm, expOrFn, cb, options) {
3    this.vm = vm
4    vm._watchers.push(this)  // 添加到当前实例的watchers内
5
6    if(options) {
7      this.deep = !!options.deep  // 是否深度监听
8      this.user = !!options.user  // 是否是user-wathcer
9      this.sync = !!options.sync  // 是否同步更新
10    }
11
12    this.active = true  // // 派发更新的标志位
13    this.cb = cb  // 回调函数
14
15    if (typeof expOrFn === 'function') {  // 如果expOrFn是函数
16      this.getter = expOrFn
17    } else {
18      this.getter = parsePath(expOrFn)  // 如果是字符串对象路径形式,返回闭包函数
19    }
20
21    ...
22
23  }
24}

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

1const bailRE = /[^\w.$]/ // 得是对象路径形式,如info.name
2
3function parsePath(path) {
4  if (bailRE.test(path))
5    return // 不匹配对象路径形式,再见
6
7  const segments = path.split('.') // 按照点分割为数组
8
9  return function (obj) {
10    // 闭包返回一个函数
11    for (let i = 0; i < segments.length; i++) {
12      if (!obj)
13        return
14      obj = obj[segments[i]] // 依次读取到实例下对象末端的值
15    }
16    return obj
17  }
18}

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

总结