vue2从数据变化到视图变化:侦听器

457 阅读2分钟

vue中数据的变化会触发一些其他行为的时候,可以使用侦听器

new Vue({
  el: '#app',
  data: {
    baseData: 'A',
  },
  template: `<div @click="changeData">{{baseData}}</div>`,
  watch: {
    baseData: function (v) {
      console.log('做些啥...');
    }
  },
  methods: {
    changeData() {
      this.baseData = 'B';
    }
  }
})

1、initWatch

在执行new Vue的过程中,执行到initState,当满足if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); }时,执行initWatch初始化侦听器:

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[key]可以是数组类型,侦听器支持数据改变的时候触发多个行为,例子中走到else逻辑,执行createWatcher(vm, key, handler)

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

侦听器还支持传入对象,将handler作为其属性,这里的主要目的是进行参数的二次处理,获取到真实的optionshandler,最后返回vm.$watch(expOrFn, handler, options)

3、$watch

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) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

先通过options.user = true传入参数,这里通过var watcher = new Watcher(vm, expOrFn, cb, options)实例化侦听器watcher
如果optionsimmediate属性的值为true则立刻执行cb回调函数。

4、Watcher

Watcher中执行expOrFn的类型为string,进而执行parsePath路径解析:

// parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

parsePath如下:

/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
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
  }
}

会返回一个以obj为参数的函数,这里的obj就是vm实例,因为:

/**
 * Evaluate the getter, and re-collect dependencies.
 */
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var 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
};

这里执行value = this.getter.call(vm, vm)的时候,第一个vm是执行主体,第二个vm就是传入的参数,即以上的obj
obj = obj[segments[i]]过程又会访问到vm上的路径,在当前例子中是this.baseData,进而触发this.baseData锁定的发布者dep,通过dep.depend实现依赖的收集,这时,depsubs就有options.user = truewatcher实例。
如果options.deeptrue,还会执行traverse

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

通过定义const seenObjects = new Set(),做已处理depId的记录,如果已经记录则终止后续操作,是一种优化。否则会定义keys = Object.keys(val),并且通过while (i--) _traverse(val[keys[i]], seen)的方式递归执行,在递归的过程中执行val[keys[i]],访问当前值,进而触发dep.notify的收集,将侦听器watch watcher推入到发布者depsubs中,当任何一个属性改变时,都会触发其侦听器watch watcher的执行,达到深度监听的目的。

5、过程渲染

在执行完侦听器的初始化后,会走到执行实例化渲染watcher的过程,这里通过_render构建vNode的时候会访问到this.baseData,继而进行渲染watcher的收集。
经过两次收集,this.baseData锁定的发布者dep中就有watch watcherrender watcher两个Watcher实例。

6、数据修改

数据修改时会触发发布者dep通知订阅者dep.notify
第一次循环最后会执行到watcherrun方法:

/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
Watcher.prototype.run = function run () {
  if (this.active) {
    var 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
      var 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);
      }
    }
  }
};

当通过var value = this.get()获取到新的值后,var oldValue = this.value定义旧值,this.value = value定义新值,这里this.usertrue,进而执行this.cb.call(this.vm, value, oldValue),将新旧值作为参数传入,这就是在侦听器中能访问到新旧值的原因。这里的this.cb就是例子中的

function (v) {
  console.log('做些啥...');
}

小结:

侦听器是当数据发生变化时可以让用户执行自定义行为,并且支持通过对象的方式传入多个handler,也可以通过传入deepimmediate来支持深度遍历和立刻执行的需求。