您的位置:首页 > Web前端 > Vue.js

Vue 响应式系统(二)- observe 工厂函数

2020-07-14 06:35 513 查看

接上篇文章回到 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
});
});

推荐:

申请即送:

  • BAT大厂面试题、独家面试工具包,

  • 资料免费领取,包括 各类面试题以及答案整理,各大厂面试真题分享!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: