Skip to content

监听器 watch 原理探索

Vue 中,watch可以让我们更灵活的监听某个数据的变化来执行一些副作用

initWatch

我们在代码里面定义了watch, 首先会在 initState 里面做初始化操作,源码如下:

ts
// src/core/instance/state.ts
initState(vm:Component) {
    const opts = vm.$options
    // ....
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch)
    }
}

若是传入的选项里面包含watch 属性,且opts.watch !== nativeWatch就会进入initWatch

nativeWatch是做了一个火狐浏览器的容错处理 FirefoxObject.prototype 上有一个“watch”功能

ts
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = {}.watch;

接着来看initWatch

ts
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    if (isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

initWatch方法首先遍历watch属性, 将每一个watch取出来, 依次调用createWatcher方法

这里还有一个细节点,watch中的一个watch对应的副作用函数可以为数组,在initWatch中会将数组内每个副作用依次调用createWatcher来创建watcher

js
export default {
  props: {
    msg: Number
  },
  data() {
    return {
      afterMag: 0
    };
  },
  watch: {
    // 数组类型的事件处理函数
     msg:[
      function add(v) {
        console.log("add",v);
      },
      function (v) {
        console.log("count", v);
      },
     function a(v) {
      this.updateAfterMsg(v)
     },
     "fn"
    ],
    // 对象类型的事件处理函数
    afterMag: {
      // immediate: true, 可选参数是否立即执行handler, 默认值false handler名字是固定的
      handler(v, old) {
        console.log(v, old);
      }
    }
  },
  methods: {
    updateAfterMsg(val) {
      this.afterMag += val;
    }
  }
};

因此关键的方法在于createWatcher

ts
function createWatcher(
  vm: Component,
  expOrFn: string | (() => any),
  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);
}

createWatcher内对传入的 hendler进行兼容处理, handler可以为 对象,字符串,函数 三种类型,得到真正的handler后,最终都会调用并返回vm.$watch(expOrFn, handler, options);, 其中expOrFn为 watch 中的 key, handler 为真正的副作用函数

到这里,需要找到实例上的$watch来源,同来自同一文件:

ts
// src/core/instance/state.ts
Vue.prototype.$watch = function (
  expOrFn: string | (() => any),
  cb: any,
  options?: Record<string, any>
): Function {
  const vm: Component = this;
  // 若传入的cb 即watch[key] 为对象,则递归调用createWatcher
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options);
  }
  options = options || {};
  options.user = true;
  // 创建watcher
  const watcher = new Watcher(vm, expOrFn, cb, options);
  if (options.immediate) {
    // 若是立即执行
    const info = `callback for immediate watcher "${watcher.expression}"`;
    pushTarget();
    // 调用时一并进行错误处理
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
    popTarget();
  }
  // 不是立即执行, 返回一个解绑函数
  return function unwatchFn() {
    watcher.teardown();
  };
};

立即执行时候会执行

ts
export function invokeWithErrorHandling(
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res;
  try {
    // 调用处理函数 handler
    res = args ? handler.apply(context, args) : handler.call(context);
    // 调用结果是promise但不是Vue实例时,进行错误的捕获
    if (res && !res._isVue && isPromise(res) && !(res as any)._handled) {
      res.catch((e) => handleError(e, vm, info + ` (Promise/async)`));
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      (res as any)._handled = true;
    }
  } catch (e: any) {
    // 错误捕获
    handleError(e, vm, info);
  }
  // 将调用的结果返回
  return res;
}

到这里应该很明确了,watch 最终就是Watcher的实现,要追踪 watch,就需要回到Watcher