Vue 响应式系统(二)- observe 工厂函数
接上篇文章回到 initData 函数的最后一句代码:
// observe data
observe(data, true /* asRootData */)
调用了 observe 函数观测数据, observe 源码如下:
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
observe 函数接收两个参数,第一个参数是要观测的数据,第二个参数是一个布尔值,代表将要被观测的数据是否是根级数据。
observe函数开始就是一个if 判断。
if (!isObject(value) || value instanceof VNode) {
return
}
如果要观测的数据不是一个对象或者是 VNode 实例,则直接 return 。
关于VNode实例可以去了解之前写的编译器相关的文章。
接下来又是一个if else 语句。
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
从字面意思不难理解,if 分支判断value 是否有"ob" 属性, 如果有是否为 Observer 实例,两者满足把 value.ob 值赋值给ob。 但为什么要这么做 ob 又是什么呢?
原因是当一个数据对象被观测之后将会在该对象上定义 ob 属性,换句话说不管哪个数据对象被观测了此对象上会扩展一个ob 属性。在此 if 分支的作用是用来避免重复观测一个数据对象。
再来看看 else...if 分支,如果数据对象上没有定义 ob 属性,那么说明该对象没有被观测过,进而会判断 else...if 分支,那么会执行 ob = new Observer(value) 对数据对象进行观测。 前提是数据对象满足所有 else...if 分支的条件才会被观测,我们看看需要满足什么条件:
- shouldObserve 为 true ( 默认值 true )
- !isServerRendering() 函数的返回值是一个布尔值,用来判断是否是服务端渲染。Vue SSR 数据预取和状态
- Array.isArray(value) || isPlainObject(value) 被观测的数据对象必须是数组或者纯对象。
- value._isVue Vue实例才拥有_isVue 属性,在此是避免观测Vue实例对象。
- Object.isExtensible(value) 观测的数据对象必须是可扩展的。(对象默认可扩展)
阻止对象扩展方法有:Object.preventExtensions() 、Object.freeze() 、Object.seal()
当一个对象满足了以上五个条件时,就会执行 else...if 语句块的代码,创建一个Observer实例:
ob = new Observer(value);
真正将数据对象转换成响应式数据对象的是 Observer 函数 。
Observer 构造函数源码:
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
Observer构造函数的实例对象将拥有三个实例属性,分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray。来看下实例化 Observer 构造函数对象都做了什么。
实例对象的 value 属性引用了数据对象:
this.value = value;
实例对象的 dep 属性获取Dep构造函数实例的引用,用于依赖收集。
this.dep =new Dep();
实例对象的 vmCount 属性初始设置为0:
this.vmCount = 0;
接下来:
def(value, '__ob__', this);
使用 def 函数,为数据对象定义了一个 ob 属性,这个属性的值就是当前 Observer 实例对象。
def 源码:
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
有意思的是这里监听你对 obj.key 属性访问, 值为val。 但是把obj.key 设置为不可枚举的属性,之所以这么做的原因是后面遍历数据对象的时候防止遍历到 ob 属性。
假设我们的数据对象如下:
var data = {count: 1};
那么经过 def 函数处理之后,data 对象应该变成如下这个样子:
var data = {
count: 1,
// __ob__ 为不可枚举的属性
__ob__: {
value: data,
dep: new Dep(),
vmCount: 0
}
}
在看接下来的代码:
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
该判断用来区分数据对象到底是数组还是一个纯对象,因为对于数组和纯对象的处理方式是不同的,先来看下如果数据对象是数组对象的处理方式。
通过以上代码了解到在数据对象是数组时,还会在做一个if...else 的判断,根据变量hasProto决定是去调用protoAugment函数 还是 copyAugment 函数,hasProto 是一个布尔值,它用来检测当前环境是否可以使用proto属性,再此我们只考虑正常情况不去扩展边界默认都为真。所以接下来进入到 protoAugment 函数中去。
protoAugment函数源码
function protoAugment(target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
这里做了什么事情?
它把我们即将要观测的数组原型链指向了src, src的值是调用protoAugment函数传过来的arrayMethods对象。
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
arrayMethods 对象的原型链又指向了Array.prototype,画个图来理解下。
如图所示,在这使用了一种叫"代理原型"的方式,在被观测的数组对象与Array.prototype之间插入一个纯对象。此时value. __ proto . proto __ === Array.prototype。
为什么要这么做?
因为数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,我们称其为变异方法,这些方法有:push、pop、shift、unshift、splice、sort以及reverse等。 这个时候我们就要考虑一件事,即当用户调用这些变异方法改变数组时需要触发依赖。并且需要知道何时调用了这些变异方法,这样才能在这些方法被调用时做对应的处理。而这步的操作就是通过代理原型的方式实现的。
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var args = [],
len = arguments.length;
while (len--) args[len] = arguments[len];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) {
ob.observeArray(inserted);
}
// notify change
ob.dep.notify();
return result
});
});
推荐:
- 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。
- 结实人脉、讨论技术 你想要的这里都有!
- 抢先入群,跑赢同龄人!(入群无需任何费用)
- 群号:779186871
- 点击此处,与前端开发大牛一起交流学习
申请即送:
-
BAT大厂面试题、独家面试工具包,
-
资料免费领取,包括 各类面试题以及答案整理,各大厂面试真题分享!