【Vue v2.5.17 源码学习】watch

140 阅读2分钟

在执行初始化数据initState方法中,如果参数中存在watch,会调用initWatch。

// instance/state.js
if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}
// instance/state.js
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)
    }
  }
}

遍历得到每个watch,然后调用createWatcher方法。

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有以下两种三法:

watch: {
  a () {
	console.log('值是方法')
  },
  b: {
  	handler () {
      console.log('值是对象')
    },
    deep: false,
    immediate: false
  },
  c: 'getC'
},
methods: {
  getC () {
	console.log('值是方法名,方法在method中')
  }
}

所以createWatcher就有针对这三种情况做了处理。最终就是获取到watch对应的方法,然后调用vm.$watch(expOrFn, handler, options)。

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

最主要一句就是

const watcher = new Watcher(vm, expOrFn, cb, options)

下面我们通过一个栗子来看:

watch: {
  a (cur, old) {
  	console.log('监控a')
  }
}

前面的逻辑比较简单,我们就从new Watcher这一句开始看,expOrFn就是a, cb就是a对应的方法。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    //省略。。。
    this.cb = cb
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      //省略。。。
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
// util/lang.js
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  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
  }
}

这里把expOrFn(也就是a)传入parsePath方法,赋值给当前watcher.getter方法,其实就是获取当然vue实例中,a属性的值(vm.a)。这里只是定义方法,还未调用。紧接着调用watcher.get()方法。

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

先把当前watcher赋值给Dep.target,然后调用getter方法,获取当前vue实例中a的值。重点来了!因为vm.a是响应式的,如果获取a的值,必定会调用a的访问器属性get方法,这是a的dep里就收集了当前的观察属性的watcher。一点属性a有变化,就会notify这个watcher,这个watcher就会update,然后排队执行watcher.run方法。

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

run方法就是重新获取一边属性的值,然后调用watcher.cb方法,也就是watch对象中a属性对应的方法,把新旧值分别传入即可。

immediate

如果在观察者属性中设置immediate为true,那么直接调用观察者属性对应的方法。

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

但是这时,参数只能获取到当前的watcher的值,旧值是获取不到的。

deep

在watcher的构造函数中会根据入参设置deep,如果用户传了入参deep为true,就在watcher.get会执行如下方法

if (this.deep) {
  traverse(value)
}
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  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
    // val[i] 就是读取值了
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    // val[keys[i]] 就是读取值了
    while (i--) _traverse(val[keys[i]], seen)
  }
}

这个方法里有取值的动作,因为属性都是响应式的,这个动作会触发属性的get方法,就把当前的watcher收集了。

总结

其实就是让用户自己新建一个watcher,把这个watcher放入要被观察的属性的dep里。这样当a发生变化时,就会notify这个watcher,最终回调到watch对象中a对应的方法。