从watcher类型来理解computed和watch的使用

735 阅读8分钟

watcher类型

Watcher 的构造函数对 options 做的了处理,代码如下:

if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.computed = !!options.computed
  this.sync = !!options.sync
  // ...
} else {
  this.deep = this.user = this.computed = this.sync = false
}

所以watcher总共有四种类型

computed

计算属性实质上等同于computed watcher

初始化Computed执行流程图和原理如下图

initComputed

计算属性的初始化时发生在Vue实例初始化阶段的initState函数中的,执行了

if(opts.computed){
    initComputed(vm,opts.computed)
}

initComputed函数的步骤:

  • 获取computed属性的getter函数
  • 创建一个computed的Watcher实例
  • 如果key已存在vm实例中(则发出警告),否则调用defineComputed
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.creat(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    //获取getter,这里可以看出computed的配置方式除了函数还能是对象
    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.
      // 创建一个computed的Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    // 从这里的判断可以看出props和data的数据已经被proxy到vm实例上,且优先级大于computed
    if (!(key in 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)
      }
    }
  }
}

从上面的代码可以注意到几点:

  1. computed的配置方式除了函数还可以是对象
  2. computed创建的Watcher实例是一个Computed watcher
  3. computed的属性不能与data和props中的重复

defineComputed

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  //如果不是SSR则默认需要cache
  const shouldCache = !isServerRendering()
  //如果是getter是函数
  if (typeof userDef === 'function') {
    //判断是否需要缓存,需要缓存则调用createComputedGetter,否则直接设置sharedPropertyDefinition.get为getter
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    //对象形式,还要判断是否配置了cache为false,由此可见computed还有cache、set配置  
    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
      )
    }
  }
  //给计算属性添加getter和stter
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

应该注意到,一个computed属性,也需要被数据劫持监听

createComputedGetter的定义


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

在defineReactive中,被劫持的数据会在getter中进行依赖收集,这里的作用也是进行依赖收集,并返回值 也就是说computed Watcher应该不仅保存着它依赖数据的Dep,还应该维护着一个Dep订阅者中心来保存依赖当前计算属性值的Watcher

Computed Watcher

Computed Watcher完成两项工作:

  • 完成计算属性与被依赖属性的绑定(Computed Watcher没有视图更新函数)
  • 保存依赖计算属性的渲染Watcher到Dep中(也就是视图层使用了计算属性的元素渲染Watcher)
export default class Watcher {
  vm: Component;      //存放vm实例
  expression: string;
  cb: Function;       //视图更新的回调函数
  lazy: boolean;      //true 下次触发时获取expOrFn当前值;false 立即获取当前值
  dirty: boolean;
  active: boolean;
  computed:boolean;   //是否为computed Watcher
  dep:Dep;            //保存依赖计算属性的Watcher
  deps: Array<Dep>;   //保存计算属性依赖数据的Dep
  newDeps: Array<Dep>;
  depIds: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    //省略
    //这里是computed Watcher的不同之处
    if (this.computed) {
       //不会立即求值,同时持有一个Dep实例
       this.value = undefined
       this.dep = new Dep()
    } else {
       this.value = this.get()
    }
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
   /*获得getter的值并且重新进行依赖收集*/
  get () {
    /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this)
    let value
    const vm = this.vm
    //调用表达式,这里的getter指的是当前watcher对应的表达式,但表达式会触发依赖数据的getter
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }
    // 这里用了touch来形容,意味着触发
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
    if (this.deep) {
      /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
      traverse(value)
    }

    /*将观察者实例从target栈中取出并设置给Dep.target*/
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * Add a dependency to this directive.
   */
   /*添加一个依赖关系到Deps集合中*/
  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)
      }
    }
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   /*
      调度者接口,当依赖发生改变的时候进行回调。
   */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
   /*
      调度者工作接口,将被调度者回调。
    */
  run () {
    if (this.active) {
      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.
        /*
            即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
        */
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        /*设置新的值*/
        this.value = value

        /*触发回调渲染视图*/
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
   /*获取观察者的值,仅用于computed watchers*/
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /**
   * Depend on all deps collected by this watcher.
   */
   /*收集该watcher的所有deps依赖,仅用于Computed Watcher*/
  depend () {
    if (this.dep && Dep.target) {
       this.dep.depend()
    }
  }
}

在Compile阶段,会对元素进行遍历,对于文本节点会查看其是否包含{{}}指令,对于元素节点会对属性进行遍历,查看是否存在v-text之类的指令,生成Watcher实例绑定指令对应的视图更新函数与被依赖数据,查看以下实例

<template>
    <div>
        <span v-text="fullName"></span>
    </div>
</template>

<script>
export default {
  data() {
    return {
        firstName:'johe',
        secondName:'test'
    }
  },
  computed:{
      fullName(){
          return this.fistName + ' ' +this.secondName
      }
  }
}
</script>

在Compile阶段,会对span的属性进行遍历,当遍历到v-text时,会为其绑定text对应的视图更新函数和被依赖数据,是通过实例化Watcher来进行绑定的。同理,当计算属性fullName在Compile阶段被触发getter时,应该收集当前被渲染元素节点span的Watcher实例,这个收集过程是通过Computed Watcher实现的

即元素节点指令访问fullName,fullName的getter触发(根据上面的computedGetter),调用Computed Watcher的depend(),depend方法调用Dep.depend() Dep.depend()会调用当前元素节点对应的Watcher.addDep(),在调用Dep.addSub收集当前渲染Wathcer

总的流程: 元素节点<span v-text="fullName"></span>生成渲染Watcher->触发计算属性fullName的getter->调用Computed Watcher的Depend->调用Dep.depend()->调用渲染Watcher的addDep收集计算属性的Dep->调用计算属性Dep.addSub收集当前渲染Watcher.完成双向绑定

回顾下Dep类:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

所以Computed Watcher不仅依赖着它的getter中的数据,例如上图的this.firstName和this.secondName,也同时被元素节点对应的Watcher依赖。

计算属性与依赖数据的绑定

fullName计算属性同时依赖着firstName和secondName,那么在fullName执行getter函数的时候,会触发firstName和secondName的getter,在getter中firstName的Dep和secondName的Dep完成了对Computed Watcher的依赖收集。 当firstName或者secondName更新时,setter函数被调用,通过Dep去发布更新的通知,调用Computed Watcher的update方法

update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
}


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

通过查看update方法,可以知道computed watcher实际上是有两种模式的,lazy和active,如果this.dep.subs.length ===0成立,说明没人人订阅计算属性的变化,仅仅把dirty设置为true(作用为下次访问计算属性的时候才会求值)。 假设目前有元素节点依赖当前计算属性,则会调用getAndInvoke()方法,方法首先对计算属性进行求值,然后再调用Dep.notify通知依赖计算属性的Watcher进行视图更新。

从源码总结

由以上可以总结:

  1. computed的配置方式除了函数还可以是对象
...
computed:{
    fullName(){
        return this.firstName + this.secondName
    }
}
...
//等同于
computed:{
    fullName:{
        get(){        
            return this.firstName + this.secondName
        }
        set(){
             ...
        }
    }
}

  1. computed getter函数内所有响应式数据的更新都会引起计算属性的更新,例如this.firstName和this.secondName
  2. computed Watcher有两种模式,当没有视图依赖计算属性的时候,当前computed Watcher处于lazy状态,否则处于active,处于lazy状态时,即使getter函数内依赖数据的更新,也不会引起计算属性的更新(下次有人订阅这个计算属性的时候再求值)
  3. 计算属性的Watcher被保存在当前vm实例的_computedWatchers中
  4. computed watcher不仅保存着getter中依赖数据的Dep,还要保存依赖当前计算属性的Watcher到Dep中,这是与其他Watcher最大的区别
  5. computed watcher本质上是对计算属性的getter和计算属性的dep发布函数(即getAndInvoke)与依赖数据的绑定

watch

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

if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}

function initWatch (vm: Component, watch: Object) {
  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)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

这一块的处理逻辑就是循环遍历watch的属性,取到对应的handler和其余配置选项,调用watch.watch是Vue原型上的方法,是在执行stateMixin的时候定义的

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    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 () {
    watcher.teardown()
  }
}

$watch实例化一个user watcher,因为options.user = ture,通过实例化watcher的方式,绑定expOrFn即watch的属性与回调函数cb,一旦expOrFn的值发生改变,就会调用watcher的run方法,执行回调函数cb。并且如果我们设置了immediate为true,会立即执行回调函数cb.最后返回一个unwatchFn方法,它会调用teardown方法移除这个watcher

user watch源码总结

从源码总结:

  1. watch支持对同一个key应用多个处理函数,所以配置项可以是一个handler数组
  2. 配置项可以是对象,hanlder.handler为回调函数,其余为配置
  3. 配置项可以是字符串,为methods中的函数名
watch: {
    flag: [function (newVal, oldVal) {
     
    }]
}
//或者
watch: {
    flag(newVal, oldVal) {
     
    }
}
//或者
watch:{
    flag:{
        handler(newVal,oldVal){
        
        }
    }
}
//或者
watch:{
    flag:{
        [
            {
                handler(newVal,oldVal){
                
                }
            }
        ]
    }
}
  1. $watch方法或者watch配置的侦听属性,都会创建一个user watcher,并且如果在配置项中使用immediate,可以使得侦听回调立即执行
  2. user wathcher本质上是被监听属性与cb的绑定,或者是包含被监听属性的函数与cb的绑定
// this.flag改变时,会调用cb
this.$watch(() => {
    return this.flag
  }, (newVal, oldVal) => {
    //do something
})

deep watcher

通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

var vm = new Vue({
  data() {
    a: {
      b: 1
    }
  },
  watch: {
    a: {
      handler(newVal) {
        console.log(newVal)
      }
    }
  }
})
vm.a.b = 2

此时不会log任何数据,因为我们watch了a对象,只触发了a对象的getter没有触发a.b对象的getter,所以a.b依赖没有收集到当前cb的依赖 通过配置属性deep,会调用traverse()函数递归的深层次访问子对象,触发他们的getter进行依赖收集(在这个时期内Dep.target都指向user watcher)

class Watcher{
    get() {
      let value = this.getter.call(vm, vm)
      // ...
      if (this.deep) {
        traverse(value)
      }
    }
}
const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  //这里可以看到如果是frozen对象,是不会访问的
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    //防止重复访问
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    //是数组则访问数组的每个成员
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    //对象则访问每个属性
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

源码总结

  • deep属性帮助我们对对象深层次的监听,但是会花费一定的性能开销。所以要权衡是否开启这个配置。
  • 如果对象被Object.freeze冻结了,即使使用deep,也无法监听

user watcher

$watch创建的是user watcher,user watcher仅仅只是增加了错误警告提示

sync watcher(同步)

当响应式数据发送变化后,触发了watcher.update(),只是把这个watcher推送到一个队列中,在nextTick后才会真正执行watcher的回调函数。而一旦我们设置了 sync,就可以在当前Tick中同步执行watcher的回调函数。

只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true。

update () {
  if (this.computed) {
    // ...
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

源码总结

  • 如果设置了sync选项为true,则会在当前Tick中同步执行wathcer回调函数,否则会在nextTick中执行

#总结 除了各项watcher的总结,我们在使用watch的时候可以用到以下配置

  • immediate:立即执行watch的回调
  • deep:深层次监听
  • sync:同步执行回调,只有当watch值的变化到执行回调函数是一个同步过程的时候才使用

各项watcher会被汇总到vm实例的_watchers中