一文搞懂computed和watch的区别

126 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天。
通过下面一个例子我们来分析computed和watch的区别。

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  },
  watch: {
      'firstName': function(newValue, oldValue) {
          console.log(newValue, oldValue)
      }
  }
})

computed

首先我们看下精简后的computed的源码

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

我们发现initComputed做了三件事情

  1. 创建空的watchers对象
  2. 遍历computed对象,根据getter函数创建一个watcher实例,然后存到watchers对象当中
  3. 调用defineComputed函数

根据我们的例子, 我们创建出来的watchers基本如下:

watchers: {
    fullName: new Watcher(
        vm,
        function () {
          return this.firstName + ' ' + this.lastName
        },
        noop,
        computedWatcherOptions
    )
}

接着分析Watcher构造函数, 我们看下精简后的watcher构造函数

class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        ...
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)

        }
        this.value = this.get()
      }

       get () {
        let value
        const vm = this.vm
        value = this.getter.call(vm, vm)
        return value
      }
  }

通过this.getter.call(vm, vm)获取到结果赋值给this.value
根据我们的例子可以获取到这样一个结果

watchers: {
    fullName: {
        value: 'Foo Bar',
        ...
    }
}

我们接着看精简后的defineComputed函数

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
   sharedPropertyDefinition.get = createComputedGetter(key)
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      return watcher.value
    }
  }
}

defineComputed做了一件事情,通过defineProperty代理到computedGetter方法。
我们可以总结一下computed:

  • 根据getter函数创建watcher实例,存到watchers对象当中
  • 通过defineProperty代理访问watchers对象中watcher实例的value属性

根据我们的例子,每次访问this.fullName, 实际上是获取到watcher的value值,即: 'Foo Bar'。
值得注意的是: 当this.fistName或者this.lastName变化时,会计算出一个新的值存到watcher的value,这里的响应式暂不细数。

watch

我们看下精简后的watch相关的代码

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    createWatcher(vm, key, handler)
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  return vm.$watch(expOrFn, handler, options)
}

 Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    const watcher = new Watcher(vm, expOrFn, cb, options)
  }
  
  class Watcher {
      update () {
         this.run()
      }
      
      run () {
          const value = this.get()
          if (value !== this.value || isObject(value) || this.deep) {
            // set new value
            const oldValue = this.value
            this.value = value
            this.cb.call(this.vm, value, oldValue)
          }
      }
  }
  

代码比较简单:
根据监测项expOrFn和回调函数cb创建watcher实例,如果检测项变化,dep会调用对应watcher的update函数,最终执行对应的cb,传递进去value和oldValue参数。

总结

就应用场景而言:

  • watch适合监测某个值的变化,在cb回调函数里面完成复杂的逻辑。
  • computed适合根据其他的响应式对象获取的一个新值。