Vue2源码学习笔记之监听器

Vue 最核心的地方就是它的响应式系统,这是它最核心的精华,也是明显区别于其他的框架像 React 、 Angular 的地方。 在上一章初始化实例属性的时候,学习了 initState 函数,这个函数里面调用了一系列初始化选项的函数,为实

目录
  1. initData
  2. observe
  3. Observer
  4. 参考资料

Vue 最核心的地方就是它的响应式系统,这是它最核心的精华,也是明显区别于其他的框架像ReactAngular的地方。

在上一章初始化实例属性的时候,学习了initState函数,这个函数里面调用了一系列初始化选项的函数,为实例对象vm初始化了很多属性,这些属性大都是和响应式系统相关的。本章主要围绕initData函数来进行,因为初始化data选项几乎涉及到了响应式系统的全部内容。

先看下initData函数在initState函数中的代码。

if (opts.data) {
  initData(vm);
} else {
  observe((vm._data = {}), true /* asRootData */);
}

判断opts中是否有data属性,如果有的话,调用initData函数初始化data选项。如果没有的话,把实例对象的私有属性_data设置空对象,并调用observe函数监听这个空对象。其实,Vue 还对外提供了$data属性让我们获取data的值,而$data代理的就是_data的值。

initData#

下面来看下initData函数的代码,这是初始化data选项的函数。

先看前面的代码。

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  // ...
}

首先声明变量data存储vm.$options.data的值,然后判断data的是不是函数,如果是函数,调用getData函数获取data的值。如果不是函数,则data应该是一个对象,返回它或者一个空对象。

现在看下getData函数的代码。

export function getData(data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget();
  try {
    return data.call(vm, vm);
  } catch (e) {
    handleError(e, vm, `data()`);
    return {};
  } finally {
    popTarget();
  }
}

使用了try...catch...finally语句来执行函数,以及捕获可能出现的错误,在try代码块中使用call方法调用data函数。这里传入了两个vm参数,第一个参数表示this指向实例对象vm,第二个参数表示传入实例对象作为函数的参数。

再看下面的代码

if (!isPlainObject(data)) {
  data = {};
  process.env.NODE_ENV !== "production" &&
    warn(
      "data functions should return an object:\n" +
        "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
      vm
    );
}

判断data是不是普通对象,如果不是,会把data设置成一个空对象,然后在非生产环境中报错,提示data的值必须是一个普通对象。

继续看下面的代码。

const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
  const key = keys[i];
  if (process.env.NODE_ENV !== "production") {
    if (methods && hasOwn(methods, key)) {
      warn(`Method "${key}" has already been defined as a data property.`, vm);
    }
  }
  if (props && hasOwn(props, key)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        `The data property "${key}" is already declared as a prop. ` +
          `Use prop default value instead.`,
        vm
      );
  } else if (!isReserved(key)) {
    proxy(vm, `_data`, key); // ! 代理 data 上的属性 vm.xxx = vm._data.xxx
  }
}

首先获取data选项的keys的值,再分别获取propsmethods的值。然后通过递减keys值的长度来循环keys的值。在非生产环境中,如果methods选项的方法名也含有相同的key值会报错,提示data中已经定义过key的值。如果props选项中也含有相同名称的key时也会报错,提示props中已经定义过key的值。

再判断key是不是 Vue 中的保留属性,Vue 中的保留属性就是以$或者_开头的属性,也就是说data选项中的数据的名称不能以$或者_开头。如果不是 Vue 的保留属性,会使用proxy函数来代理data上的属性。

下面看下proxy函数的代码。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};
 
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

这个函数使得实例对象vm可以代理data选项的数据。比如,在我们访问this.xxx数据时,其实访问的是this.data.xxx数据。

vm除了会代理data选项的数据,还有代理propsmethodscomputed等选项中的值,所以这些选项里面的属性(方法)名不能重名。通过代理选项中属性,我们会更方便的使用这些选项上的属性(方法)。

这里注意一下函数中的警告信息,在属性命名的优先等级上,Vue 认为props中的属性优先级应该比data高,而data的属性优先级应该比methods高。

再看最后一行代码。

// observe data
observe(data, true /* asRootData */);

使用observe函数监听data选项,把data选项的数据变成成响应式数据。这里传入第二个参数为true,作为根组件的data选项。

其实,这句代码才是响应式系统的开始。

observe#

下面查看observe函数的代码,这是用于监听数据的函数。

先看下前面的代码。

export function observe(value: any, asRootData: ?boolean): Observer | void {
  // ! 非虚拟节点的对象
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  // ...
}

先看函数的参数,只有两个参数。第一个参数是需要监听的对象,第二个参数是一个布尔值,用来判断监听的对象是否是根组件数据。

函数第一行代码判断语句定义了监听数据value的类型,只有是对象类型且不是虚拟节点才符合要求监听的要求,如果不能同时满足这两个条件,直接返回。

再看下面的代码。

let ob: Observer | void;
 
// ! 已经是监听对象了,获取 __ob__,不需要重复监听
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
  ob = value.__ob__;
} else if (
  shouldObserve && // ! 这个属性可以自由的控制对象是否监听
  !isServerRendering() &&
  (Array.isArray(value) || isPlainObject(value)) &&
  Object.isExtensible(value) && // ! 对象可扩展,没有被 freeze
  !value._isVue // ! 不是 Vue 实例
) {
  ob = new Observer(value); // ! 实例化一个监听对象
}

先声明变量ob,它的类型是Observer或者void

然后判断value对象中是否有__ob__属性且__ob__必须是Observer类型的,如果满足这两个条件,说明value已经是一个监听的对象,不需要再重复监听,把value.__ob__属性赋值给ob即可。如果value不是监听对象,还必须要符合下面的五个条件,才能去监听value对象。

我们先看下这五个条件,对象value必须全部满足这五个条件才能被监听。

  1. shouldObserve:这是一个开关,默认为true。在某些特殊场景时,它的值会变成false,这里必须是true,可以通过toggleObserving函数来切换shouldObserve的值,这样我们就可以非常方便的控制哪些对象是否执行监听,这个条件很重要。
/**
 * In some cases we may want to disable observation inside a component's
 * update computation.
 */
export let shouldObserve: boolean = true;
 
export function toggleObserving(value: boolean) {
  shouldObserve = value;
}
  1. !isServerRendering():调用isServerRendering函数可以判断当前是否处于服务端渲染的环境中。如果值为true,说明当前宿主环境不是浏览器环境,不符合监听条件,它的值必须是false才行。
  2. (Array.isArray(value) || isPlainObject(value)):监听的value必须是数组或者普通对象。
  3. Object.isExtensible(value):监听的对象必须是一个可扩展的值。使一个对象变成不可扩展,有以下方法:Object.preventExtensions()Object.freeze()以及Object.seal(),监听的对象必须没有使用过这些方法。
  4. !value._isVue:监听的对象不能是 Vue 的实例对象。在我们初始化 Vue 的实例对象时会添加一个_isVue属性,这个属性是一个标识,说明该对象是 Vue 的实例对象。

只有同时满足了上面的五个条件,才会调用new方法创建监听器对象,创建的时候需要把监听的对象value传入,并赋值给变量ob

ob = new Observer(value); // ! 实例化一个监听对象

Observer#

下面看下Observer监听器的代码,这是一个类,用来创建监听器实例对象。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
 
  constructor(value: any) {
    // ...
  }
  walk() {
    // ...
  }
  observeArray() {
    // ...
  }
}

Observer类有三个属性和两个方法,三个属性分别是valuedepvmCount,两个方法分别是walkobserveArray

先看下类的构造函数,看前面的代码。

constructor(value: any) {
 	this.value = value
	this.dep = new Dep()
	this.vmCount = 0
  // ...
}

先初始化三个属性的值,其中value的值是引用传进来的参数value,也就是需要监听的对象。

dep是订阅器Dep的实例对象,主要是用来收集依赖(观察者)的。vmCount是组件标识,根组件的初始值是 0。

继续看下面的代码。

def(value, "__ob__", this); // ! 自身实例添加到监听对象的 __ob__ 属性上 (__ob__监听对象标识)

使用def函数定义监听的对象value的属性__ob__Observer实例对象本身。这里使用了循环引用,监听的对象value中有属性__ob__,它的值是Observer实例对象,而实例对象中也有value属性,它的值就是监听的对象value。这样我们可以很方便的从__ob__属性中获取它监听的对象value的值。

实例代码:

const data = {
  a: 1,
  // __ob__ 是不可枚举的属性
  __ob__: {
    value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
    dep: dep实例对象, // new Dep()
    vmCount: 0,
  },
};

看下def函数的代码。

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}

def函数其实就是Object.defineProperty的简单封装,它的第四个参数enumerable是一个布尔值,用来设置定义的对象是否可以枚举。

继续看下面的代码。

// ! 监听数组时
if (Array.isArray(value)) {
  // ! 判断当前环境的对象中是否有 __proto__ 属性 (是否支持使用 __proto__)
  if (hasProto) {
    protoAugment(value, arrayMethods);
  } else {
    copyAugment(value, arrayMethods, arrayKeys);
  }
  this.observeArray(value); // ! 再次监听数组,监听一些嵌套的数组
} else {
  this.walk(value); // ! 监听对象
}

接下来是一个if语句,判断监听的对象的类型,如果监听的对象是一个数组,执行if中的代码块,否则执行else代码块。

先看else代码块,也就是监听的对象是一个对象时,调用类的walk方法,并把监听的对象value作为参数传入,这个函数是用来监听对象类型的监听对象,它会把监听的对象中的属性变成响应式属性。

下面查看类的walk方法的代码。

walk(obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

首先获取传入的参数对象objkeys值,这里指监听的对象value

然后遍历keys,在每次循环时调用了defineReactive函数,同时把对象obj和它的每一个key值作为参数传入。

defineReactive函数的作用就是把对象obj中的每一个key值都变成响应式属性。

参考资料#