Vue2源码学习笔记之侦听器

Vue 给我们提供了侦听器方法 vm.$watch 和选项 watch ,用来观察 Vue 实例变化,然后执行一些操作。 下面看下它们是怎么实现的,其实它们都是基于观察者 watcher 的封装。 先看 Vue.prototype.$wat

目录
  1. vm.$watch
    1. createWatcher
  2. watch
  3. 深度监听
  4. 参考资料

Vue 给我们提供了侦听器方法vm.$watch和选项watch,用来观察 Vue 实例变化,然后执行一些操作。

下面看下它们是怎么实现的,其实它们都是基于观察者watcher的封装。

vm.$watch#

先看Vue.prototype.$watch方法的代码,在core/instance/state.js文件中。

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // ...
};

首先看下$watch方法的参数,有三个参数,分别是expOrFncboptions,我们发现这三个参数对应着Watcher构造函数的第二个到第四个参数。

继续看下面的代码。

const vm: Component = this;
 
// ! cb 是对象,调用 createWatcher 方法
if (isPlainObject(cb)) {
  return createWatcher(vm, expOrFn, cb, options);
}
// ...

声明常量vm存储当前的实例对象,即this。然后判断cb是不是一个普通对象,如果是的话,调用createWatcher函数,传入vm以及$watch原来的三个参数。

createWatcher#

先看下createWatcher函数的代码。

// ! 创建 watcher 的工厂函数
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // ! 传入的 handler 是对象时
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler; // ! 获取对象里的 handler 方法作为 cb
  }
  // ! 传入的 handler 是字符串时  => watch: { name: 'cbName' }
  if (typeof handler === "string") {
    handler = vm[handler]; // ! 获取相同名字的函数作为 cb
  }
  return vm.$watch(expOrFn, handler, options); // ! 最后调用实例的 $watch 方法
}

这个函数主要处理在$watch方法中传入的cb参数不是函数的情况,它的第三个参数handler对应$watch的第三个参数cb

这里再次判断handler是不是一个普通对象,如果是的话,函数的第四个参数options缓存第三个参数handler的值,而第三个参数handler的值为它的handler属性,即回调函数。

然后判断handler是不是字符串,如果是的话,handler的值为实例对象的handler属性,即去实例对象获取名为handler的方法作为回调函数。

最后再次调用vm.$watch方法,并把handler作为函数的第二个参数即回调函数cb传入。

小结createWatcher函数就是通过不是函数类型的cb得到函数类型的handler,然后再次调用vm.$watch方法。经过这个方法处理后,vm.$watch方法的第三个参数cb就一定是函数。

然后又回到wm.$watch方法中,继续看下面的代码。

options = options || {};
options.user = true; // ! 设置 user 为 true,说明是用户自定义的观察者
const watcher = new Watcher(vm, expOrFn, cb, options); // ! 创建观察者实例,并传入设置的参数
 
// ! 如果设置了 immediate
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(); // ! 移除 watcher
};

重新定义options为传入的options参数或者是一个空对象(没传入参数)。

设置options.user属性为true,说明下面创建的观察者对象是用户定义的观察者,然后通过new创建一个观察者实例对象,把当前的组件实例vm$mount方法的三个参数依次传入。

判断options选项中immediate属性中是否为真,如果是的话,立即执行一次回调函数cb,使用call执行,函数指向当前的实例对象,并传入参数watcher.value,即观察者的新值。

$watch方法最后返回一个函数,这个函数是用来取消观察,删除观察者的。函数调用观察者的teardown方法。

看下teardown方法的代码,在core/observer/watcher.js文件中。

teardown() {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false // ! 设置观察者实例是非激活状态
  }
}

判断active是否为真,即判断观察者是否是激活状态,如果是的话,再判断观察者所属实例对象是否已被销毁,如果没有销毁的话,把这个观察者从实例对象中的_watchers属性中删除,即销毁它。

然后通过递减观察者deps的长度的方式遍历deps,在deps里面存在观察者的dep里删除观察者,最后把观察者的active属性设为false,表示观察者是非激活状态,这样观察者的回调函数就不会执行了。

示例对象的$watch方法示例代码。

// cb 是函数时
vm.$watch(
  "name",
  () => {
    console.log("change");
  },
  { immediate: true }
);
 
// cb 是对象时
vm.$watch("name", {
  handler: () => {
    console.log("change");
  },
  immediate: true,
});

watch#

我们可以使用vm.$watch观察 Vue 实例变化,也可以使用watch选项做相同的事情。watch选项是一个对象,对象的键是需要观察的数据,值是对应的回调函数或者处理器对象,也可以是已经声明的方法的名称。

在 Vue 的实例对象初始化时,通过遍历watch选项中,然后创建观察者。

查看initWatch函数的代码,在core/instance/state.js文件中。

// ! 初始化 watch 选项
function initWatch(vm: Component, watch: Object) {
  // ! 遍历 watch
  for (const key in watch) {
    const handler = watch[key]; // ! 获取 handler
 
    // ! 如果 handler 是数组(多个观察者),遍历数组后分别创建 Watcher 实例
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

遍历watch选项,通过key获取到它的值handler

然后判断handler是不是数组,如果是数组的话,再遍历一次,在每次循环的过程中,调用createWatcher函数创建观察者。如果不是数组的话,直接调用createWatcher函数创建观察者。

通过createWatcher函数处理各种类型的handler,包括函数、对象、字符串,最后创建观察者。

组件中watch选项的示例代码,这里使用官方文档的例子。

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5,
      },
    },
  },
  watch: {
    a: function (val, oldVal) {
      console.log("new: %s, old: %s", val, oldVal);
    },
    // 方法名
    b: "someMethod",
    // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    c: {
      handler: function (val, oldVal) {
        /* ... */
      },
      deep: true,
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: "someMethod",
      immediate: true,
    },
    // 多个 watcher
    e: [
      "handle1",
      function handle2(val, oldVal) {
        /* ... */
      },
      {
        handler: function handle3(val, oldVal) {
          /* ... */
        },
        /* ... */
      },
    ],
    // watch vm.e.f's value: {g: 5}
    "e.f": function (val, oldVal) {
      /* ... */
    },
  },
});
vm.a = 2; // => new: 2, old: 1

深度监听#

在观察者的get方法中,如果this.deep的值为true会进行深度监听,深度监听的目标是对象或者数组。

get() {
  try {
    // ...
  } catch (e) {
    //...
  } finally {
    // ! 如果是深度观察 => 用于 watch 选项
    if (this.deep) {
      traverse(value) // ! 递归去访问 value,触发它所有子项的 getter
    }
    // ...
  }
 
}

通过traverse函数去递归访问value的值,触发它所有子项的getter

看下traverse函数的代码。

const seenObjects = new Set(); // ! 存储 id 值的集合
export function traverse(val: any) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

调用_traverse方法,把valseenObjects集合传入,最后调用seenObjects对象的clear方法清空seenObjects集合的元素。

再看下_traverse函数的代码。

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;
  }
  // ...
}

先声明变量ikeys。再声明常量isAisA是用来判断传入的参数val是否是数组。

判断val的值的类型,如果val是非数组非对象类型,或者是不可扩展的,或者是一个VNode,直接返回。

继续看下面的代码。

if (val.__ob__) {
  const depId = val.__ob__.dep.id;
 
  // ! 已经遍历的对象,不会再遍历,避免循环引用
  if (seen.has(depId)) {
    return;
  }
  seen.add(depId);
}

判断val是否存在__ob__属性,即它是一个响应式对象,如果是的话,声明常量depId存储从val.__ob__属性获取到的depid,这个iddep的唯一标识。

判断集合seen中是否存在depId,如果存在,直接返回,这说明dep已经收集过依赖,不需要再重复收集。如果seen中不存在depId,把depId添加到集合seen中。

继续看下面的代码。

// ! 数组
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);
}

深度观测的对象只能是对象或者数组,不管是哪种类型都是遍历它,然后访问它的每一个子项,触发子项的getter,收集依赖。

参考资料#