vue中Computed和Watch的区别

computed

用法
var vm = new Vue({
  data: { a: 1 },
  computed: {
    // 仅读取
    aDouble: function () {
      return this.a * 2
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
vm.aPlus   // => 2
vm.aPlus = 3
vm.a       // => 2
vm.aDouble // => 4

computed选项的属性值可以是一个函数,该函数默认为取值器getter,用于仅读取数据;还可以是一个对象,对象里面有取值器getter和存值器setter,用于读取和设置数据

源码分析

function initComputed (vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)
    const isSSR = isServerRendering()

    for (const key in computed) {
        const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
            warn(
                `Getter is missing for computed property "${key}".`,
                vm
            )
        }

        if (!isSSR) {
            // create internal watcher for the computed property.
          // 则创建一个watcher实例,并将当前循环到的的属性名作为键,
          // 创建的watcher实例作为值存入watchers对象中。
            watchers[key] = new Watcher(
                vm,
                getter || noop,
                noop,
                computedWatcherOptions
            )
        }

        if (!(key in vm)) {
            // 实例vm上设置计算属性
            defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
          // 这些是判断是否重名的
            if (key in vm.$data) {
                warn(`The computed property "${key}" is already defined in data.`, vm)
            } else if (vm.$options.props && key in vm.$options.props) {
                warn(`The computed property "${key}" is already defined as a prop.`, vm)
            }
        }
    }
}

● 首先会在实例上创建一个_computedWatchers对象,这个对象会存放每个computed 类watcher
● 遍历computed对象上的每个属性,如果该属性是一个函数,则默认取值器getter是这个函数,如果该属性是一个对象,则取值器getter是对象的getter属性,如果没有取值器的话,会警告
● 对每一个computed key设置一个watcher,将setter传进去,。。。。
● 设置完之后,判断实例上有没有key这个属性,没有的话就设置在实例上,使用的是defineComputed函数

defineComputed函数

该函数接受3个参数,分别是:target、key和userDef。其作用是为target上定义一个属性key,并且属性key的getter和setter根据userDef的值来设置。下面我们就来看一下该函数的具体逻辑。

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

export function defineComputed (target,key,userDef) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

该函数的主要作用就是给computed属性设置一个具有缓存功能的setter函数,在createComputedGetter函数里面设置,但是在服务端渲染的情况下不需要缓存???

createComputedGetter函数
function createComputedGetter (key) {
    return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    }
}

这里面会返回一个函数,这个函数就是该计算属性值的getter,在这个函数中,会先拿出对应的watcher出来,在执行depend和evalute方法,也就是说,computed的缓存的奥秘就在watcher的这两个方法中
接着,我们在来回顾下watcher类的定义,了解computed的缓存

watcher类
export default class Watcher {
    constructor (vm,expOrFn,cb,options,isRenderWatcher) {
        if (options) {
            // ...
            this.computed = !!options.computed
            // ...
        } else {
            // ...
        }

        this.dirty = this.computed // for computed watchers
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        }

        if (this.computed) {
            this.value = undefined
            this.dep = new Dep()
        }
    }

    evaluate () {
        if (this.dirty) {
            this.value = this.get()
            this.dirty = false
        }
        return this.value
    }

  // 收集dep
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

    /**
     * Depend on this watcher. Only for computed property watchers.
     */
    depend () {
        if (this.dep && Dep.target) {
            this.dep.depend()
        }
    }

    update () {
        if (this.computed) {
            if (this.dep.subs.length === 0) {
                this.dirty = true
            } else {
                this.getAndInvoke(() => {
                    this.dep.notify()
                })
            }
        }
    }

    getAndInvoke (cb: Function) {
        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
            this.dirty = false
            if (this.user) {
                try {
                    cb.call(this.vm, value, oldValue)
                } catch (e) {
                    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                }
            } else {
                cb.call(this.vm, value, oldValue)
            }
        }
    }
}

● 对于watcher类来说,有专门针对computed 类型的wathcer的一些方法
● 会通过this.computed属性判断是不是一个computed类型的watcher
● 如果是的话,会创建一个deps数组来收集dep,同时,会设置一个this.dirty属性来判断是否使用缓存数据,如果dirty为false(表示数据不脏)的话,取缓存,dirty为true的话不取缓存
可以看到,在实例化Watcher类的时候,第四个参数传入了一个对象computedWatcherOptions = { computed: true },该对象中的computed属性标志着这个watcher实例是计算属性的watcher实例,即Watcher类中的this.computed属性,同时类中还定义了this.dirty属性用于标志计算属性的返回值是否有变化,计算属性的缓存就是通过这个属性来判断的,每当计算属性依赖的数据发生变化时,会将this.dirty属性设置为true,这样下一次读取计算属性时,会重新计算结果返回,否则直接返回之前的计算结果。
当调用watcher.depend()方法时,会将读取计算属性的那个watcher添加到计算属性的watcher实例的依赖列表中,当计算属性中用到的数据发生变化时,计算属性的watcher实例就会执行watcher.update()方法,在update方法中会判断当前的watcher是不是计算属性的watcher,如果是则调用getAndInvoke去对比计算属性的返回值是否发生了变化,如果真的发生变化,则执行回调,通知那些读取计算属性的watcher重新执行渲染逻辑。
当调用watcher.evaluate()方法时,会先判断this.dirty是否为true,如果为true,则表明计算属性所依赖的数据发生了变化,则调用this.get()重新获取计算结果最后返回;如果为false,则直接返回之前的计算结果。

watch

用法
var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // methods选项中的方法名
    b: 'someMethod',
    // 深度侦听,该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: 'someMethod',
      immediate: true
    },
    // 调用多个回调
    e: [
      'handle1',
      function handle2 (val, oldVal) { /* ... */ },
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
      }
    ],
    // 侦听表达式
    'e.f': function (val, oldVal) { /* ... */ }
  }
})
vm.a = 2 // => new: 2, old: 1

● 可以看出,watch对象的属性表示的是侦听的属性,这个属性也可以是侦听表达式
● 每个属性值可以直接是一个函数,函数参数是newVal和oldVal,当属性变化的时候会执行该函数
● 也可以是一个字符串,表示的是methods中的方法名
● 也可以是一个对象,配置一些属性,handler表示的是数据发生变化时要执行的函数

源码分析

function initWatch (vm, watch) {
  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)
    }
  }
}
createWatcher函数
function createWatcher (
  vm: Component,  // 当前实例
  expOrFn: string | Function,  // 被侦听的属性表达式
  handler: any,  // watch选项中每一项的值
  options?: Object  // 用于传递给vm.$watch的选项对象
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 这种写法对应handler是method中的方法
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
其实就是调用了vm.$watch这个方法,将监听的属性,回调函数作为参数
vm.$watch函数
Vue.prototype.$watch = function (expOrFn,cb,options) {
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 从用户合起来传入的对象中把回调函数cb和参数options剥离出来
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
  // 用户手动调动该方法创建出来的
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // 立即执行回调
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      // 取消观察,就是将所依赖的dep中将自己删除
      watcher.teardown()
    }
  }

computed和watch的区别

computed的缓存

computed的缓存说的是只有计算属性的依赖发生变化的时候,计算属性才会去重新计算它的值,也就是说,如果页面重新渲染而所依赖的数据没有变化的时候,计算属性不会重新计算,而方法则会重新执行
这对一些需要处理很多数据的计算属性来说,可以提高性能

对于computed:

● 它支持缓存,只有当依赖的数据发生变化,才会重新计算
● 不支持异步,当computed中有异步操作时,无法监听数据的变化。因为计算属性一般是通过return返回计算属性值的,所以异步的时候不会返回正确的值
● computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
● 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
● 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。

对于watch:

● 不支持缓存,数据变化时,会触发相应的操作
● 支持异步监听,也就是可以再处理函数中使用异步操作
● 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
● 当一个属性发生变化时,就需要执行相应的操作
● 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
○ immediate:组件加载立即触发回调函数
○ deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,加上deep的话,数组内元素的变化或者对象属性值的变化,都能狗侦测的到。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

参考链接:
https://vue-js.com/learn-vue/instanceMethods/data.html#_1-vm-watch
https://www.yuque.com/cuggz/interview/hswu8g#d72e59d5f3d78b8cf5d8038e0e12803e

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值