Skip to content

侦听器watch

侦听器的使用形式

  1. 函数
js
watch: {
    // 侦听器name是一个函数
    name(val, oldVal) {
        // do something
    }
}
  1. 数组函数
js
watch: {
  // 侦听器name是一个函数数组
  name: [
    function (val, oldval) {
      /* .... */
    },
    function (val, oldval) {
      /* .... */
    }
  ];
}
  1. 函数对象
js
watch: {
  name: [
    {
      handler(val, oldval) {
        /* ... */
      },
      deep: true,
      immediate: true
    }
    // {...}
  ];
}
  1. 字符串
js
watch: {
    name: 'computedNamePath'
},
methods: {
    computedNamePath(val, oldVal) {/* .... */}
}
  1. 对象
js
watch: {
    name: {
        handler(val, oldval) {/* ... */}
        deep: true,
        immediate: true
    }
}

initWatch

前面在initState里面已经定义好了 Vue 对数据拦截的时候的顺序, props -> methods -> data -> computed -> watch, 前面已经写了initData, 现在来实现initWatch

js
// state.js

function initWatch(vm) {
  const watch = vm.$options.watch;
  for (const key in watch) {
    if (Array.isArray(watch[key])) {
      for (let i = 0; i < watch[key].length; i++) {
        const handler = watch[key][i];
        createWatch(vm, key, handler);
      }
    } else {
      createWatch(vm, key, watch[key]);
    }
  }
}

createWatch

js
function createWatch(vm, key, handler, opt) {
  if (typeof handler === "object") {
    opt = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(key, handler, opt);
}

vm.$watch

$watch是实例方法, 因此还需要通过 mixinXXX 来进行拓展

js
// state.js
// 与状态相关的Mixin
export function stateMixin(Vue) {
  // 拓展$watch
  Vue.prototype.$watch = function (key, handler, opt) {
    const vm = this;

    const options = {};
    options.user = true;
    if (!opt) {
      opt = {};
    }
    new Watcher(vm, key, handler, { ...options, ...opt });
  };
}

到这里, watch侦听器的原理已经很明显, 就是一个watcher, Watcher也有了第二个角色, 侦听器watcher, 侦听器watcher有一个特点, 就是usertrue

侦听器 watcher 依赖收集

先看 watch 里面的逻辑

js
let uid = 0;
class Watcher {
  constructor(vm, exprFn, cb, options) {
    this.vm = vm;
    this.cb = cb; // 回调函数
    // this.exprFn = exprFn; // 组件真是的渲染函数
    this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
    this.depsIds = new Set(); // 一个watcher上面有多个dep, 且不重复
    this.deps = [];
    this.user = !!options.user;
    this.options = !!options; // 是否为渲染watcher

    if (this.user) {
      // 若是watch, 则将watch的handler做为watcher的cb, 在依赖更新的时候调用这个cb
      this.getter = function () {
        let obj = vm;
        const path = exprFn.split(".");
        for (let i = 0; i < path.length; i++) {
          obj = obj[path[i]];
        }
        return obj;
      };
    } else {
      this.getter = exprFn;
    }

    this.value = this.get(); // 首先就让组件渲染函数执行
  }

  run() {
    const newVal = this.get();
    if (newVal !== this.value) { // 对比老值与新值
      this.value = newVal;
      this.cb.call(this.vm, newVal, this.value); // 调用watch的handler
    }
  }
  /* .....省略一些方法..... */
  get() {
    pushTarget(this);
    let val = this.getter();
    popTarget();
    return val;
  }
}

export default Watcher;

Watcher内部逻辑进行了一定的改写, new Watcher的时候,会先判断是否为watch, 即user是否为true, 将watch的建取值的操作封装为一个待执行的函数, 然后重新给getter赋值为watchkey取值的操作的函数, 并执行这个函数, 得到最新的值, 存到watcher.value

接着调用这个取值函数, 取值会触发Object.definePropertyget方法, 从而收集了依赖, 这部分代码是服用了初始化data时候的依赖收集

当值被更改的时候, 会触发set函数, 最终触发watcher.run(), 在run中可以拿到最新的值, 与老值进行比较, 若是不一样则需要调用cb