Cosen95 / blog

关注行业前沿,分享所见所学。持续输出优质文章 :rocket:
212 stars 15 forks source link

Vue源码探秘(watch) #37

Open Cosen95 opened 4 years ago

Cosen95 commented 4 years ago

引言

上一节我们分析了计算属性computed,这一节我们一起来看下侦听器watch

大家平时在项目开发中,有时会对这两种特性感到模棱两可,不知道该选择那个。相信看完本篇文章,结合上一篇,你会有一个答案的。

让我们开始吧!

watch

首先组件实例化过程中,会调用initState函数:

// src/core/instance/state.js

export function initState(vm: Component) {
  // ...
  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);
    }
  }
}

initWatch 函数会遍历 watch 的属性然后调用 createWatcher 函数。这里判断了 watch[key] 是数组的情况。

Vue 是支持 watch 的同一个 key 对应多个 handler,也就是handler是一个数组

如果是数组需要遍历数组每一项再调用 createWatcher 函数。来看 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);
}

createWatcherhandler分别是对象和字符串的情况,进行了处理,统一将handler转成函数并调用 vm.$watch 函数。$watch 方法定义在 Vue 原型上:

// src/core/instance/state.js
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);
  // ...
};

这里$watch先判断传入的cb是不是对象,如果是对象则继续调用createWatcher进行参数规范化处理。

这里是因为$watch 是个对外暴露的接口,我们平时开发也可以直接使用 $watch 函数来取代 watch 属性。

这里传入的 options 参数是 undefined ,所以会给 options 创建一个空对象并且 options.user = true

接着将 options 传入 Watcher 创建一个 user Watcher 实例。我们来看 user Watcher 的实例化过程:

export default class Watcher {
  // ...

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  this.vm = vm
  if (isRenderWatcher) {
    vm._watcher = this
  }
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
    this.before = options.before
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  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)
    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
      )
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get()
}

这里因为不是渲染 Watcher ,所以 if (isRenderWatcher) 不会执行,接着在 if (options) 逻辑中初始化属性值,其中 this.user = true 表明了这是一个 user Watcher ,而其他的像 deepsync 对应watch中的配置项。

接着会判断传入的 expOrFn 是不是函数,如果是则赋值给 this.getter 。不是的话走 else 逻辑调用 parsePath 函数。

我们来看 parsePath 函数的定义:

// src/core/util/lang.js
/**
 * 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;
  };
}

parsePath 函数首先会通过正则解析这个 path 看是否合法。如果不合法,直接return。如果合法会将 path 拆成数组。最后 parsePath 函数会返回一个函数赋值给 user Watchergetter 属性。

返回的函数的逻辑也很简单,就是根据解析出来的路径去访问传入的 obj 参数对应的属性值并返回出去。

回到 user Watcher 的实例化过程,最后这里会执行 this.get 方法:

/**
 * Evaluate the getter, and re-collect dependencies.
 */
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
}

get 方法首先执行 pushTarget() 。我们知道 pushTarget 会修改 Dep.target 为传入的 this 也就是 user Watcher

接着会执行 this.getter.call(vm, vm)getter 就是前面 parsePath 函数返回的函数:

return function (obj) {
  for (let i = 0; i < segments.length; i++) {
    if (!obj) return;
    obj = obj[segments[i]];
  }
  return obj;
};

之后会有一个判断 this.deep 的逻辑,如果有配置deeptrue,这里就会执行 traverse(value)traverse 函数定义如下:

// src/core/observer/traverse.js
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);
  }
}

traverse 这里调用了_traverse 函数。_traverse 函数会对传入的 val 参数进行校验,即 val 满足下面三个条件的话直接return

接着有 if (val.__ob__)if (isA) else 两大段逻辑,我们先分析 if (isA) else 。这段逻辑就是针对 val 是数组或者是对象分别做遍历,遍历它们的元素或者属性递归调用 _traverse 函数。

我们再回过头来看 if (val.__ob__) 这段逻辑,它的作用是把 val__ob__.dep.id 保存到 seen 也就是在全局定义的 seenObjects 中,并且还通过 if (seen.has(depId)) 逻辑防止重复保存。那什么情况会重复保存呢,那就是循环引用,比如下面这个例子:

const obj1 = {};
const obj2 = {};
obj1.a = obj2;
obj2.a = obj1;

如果没有 if (seen.has(depId)) 的逻辑,那么上述例子会在 if (isA) elseelse 的逻辑中一直不停地执行。

这样 _traverse 函数的逻辑就分析完了。其实实现深度观测很简单,就是深度遍历数组或者对象,相当于深度访问了数组(对象)的所有元素(属性)。这样就会触发它们的 getter 去收集依赖,把 user Watcher 收集进来。

回到 traverse 函数,在执行完 _traverse 函数后就将 seenObjects 清空。这样 user Watcher 的实例化过程就分析完了。回到 $watch 函数:

// src/core/instance/state.js
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // ...
  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();
  };
};

在创建完 user Watcher 之后,$watch 会判断 options.immediate 是否为 true。如果为 true ,则会立即执行 cb 也就是我们编写的回调函数。

最后 $watch 函数返回一个函数,这个函数会执行 watcher.teardown 来解除当前观察者对属性的观察。

以上就是 watch 的初始化渲染过程,其原理就是访问被 watch 的数据触发其 getter ,使得 user Watcher 被收集,在被 watch 的数据改变时就能触发 setter 通知 user Watcher 执行回调。

当被 watch 的数据发生变化时会触发 setter 派发更新,我们知道派发更新就是通知订阅者 user Watcher 去执行 update 方法:

// src/core/observer/watcher.js
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

这里如果synctrue,那么就会执行run函数:

// src/core/observer/watcher.js
/**
 * 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.
      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 函数会对比被 watch 的数据有没有发生变化,如果有就立即执行回调函数。

如果没有配置 synctrue ,那么在 update 函数中就会走 else 逻辑执行 queueWatcher 函数。

queueWatcher 函数的具体执行过程以及其中的 nextTick 函数我们在 Vue源码探秘(派发更新)Vue源码探秘(nextTick) 已经分析过了,简单来讲,会异步执行 watch 的回调函数。

总结

结合上一节的computed,我们这里简单对比下watchcomputed