Vue 最核心的地方就是它的响应式系统,这是它最核心的精华,也是明显区别于其他的框架像React、Angular的地方。
在上一章初始化实例属性的时候,学习了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的值,再分别获取props和methods的值。然后通过递减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选项的数据,还有代理props、methods和computed等选项中的值,所以这些选项里面的属性(方法)名不能重名。通过代理选项中属性,我们会更方便的使用这些选项上的属性(方法)。
这里注意一下函数中的警告信息,在属性命名的优先等级上,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必须全部满足这五个条件才能被监听。
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;
}!isServerRendering():调用isServerRendering函数可以判断当前是否处于服务端渲染的环境中。如果值为true,说明当前宿主环境不是浏览器环境,不符合监听条件,它的值必须是false才行。(Array.isArray(value) || isPlainObject(value)):监听的value必须是数组或者普通对象。Object.isExtensible(value):监听的对象必须是一个可扩展的值。使一个对象变成不可扩展,有以下方法:Object.preventExtensions()、Object.freeze()以及Object.seal(),监听的对象必须没有使用过这些方法。!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类有三个属性和两个方法,三个属性分别是value、dep和vmCount,两个方法分别是walk和observeArray。
先看下类的构造函数,看前面的代码。
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])
}
}首先获取传入的参数对象obj的keys值,这里指监听的对象value。
然后遍历keys,在每次循环时调用了defineReactive函数,同时把对象obj和它的每一个key值作为参数传入。
defineReactive函数的作用就是把对象obj中的每一个key值都变成响应式属性。