前言
上一篇文章,大概的讲解了Vue实例化前的一些配置,如果没有看到上一篇,通道在这里:Vue 源码解析 - 实例化 Vue 前(一)
在上一篇的结尾,我说这一篇后着重讲一下 defineReactive 这个方法,这个方法,其实就是大家可以在外面看见一些文章对 vue 实现响应式数据原理的过程。
在这里,根据源码,我决定在给大家讲一遍,看看和大家平时自己看的,有没有区别,如果有遗漏的点,欢迎评论
正文
先来一段 defineReactive 的源码:
//在Object上定义反应属性。
function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
var getter = property && property.get;
if (!getter && arguments.length === 2) {
val = obj[key];
}
var setter = property && property.set;
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
复制代码
在讲解这段源码之前,我想先在开始讲一下 Object 的两个方法 Object.defineProperty() 和 Object.getOwnPropertyDescriptor()
虽然很多前端的大佬知道它的作用,但是我相信还是有一些朋友是不认识的,我希望我写的文章,不只是传达vue内部实现的一些精神,更能帮助一些小白去了解一些原生的api。
defineProperty
在 MDN 上的解释是:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
复制代码
这里,其实就是用来实现响应式数据的核心之一,主要做的事情就是数据的更新, Object.defineProperty() 最多接收三个参数:obj , prop , descriptor:
obj:
要在其上定义属性的对象。
复制代码
prop:
要定义或修改的属性的名称。
复制代码
descriptor:
将被定义或修改的属性描述符。
复制代码
返回值:
被传递给函数的对象。
复制代码
在这里要注意一点:在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义key为Symbol的属性的方法之一。
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
configurable:
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。
默认值: false
复制代码
enumerable:
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。
默认为 false。
复制代码
数据描述符同时具有以下可选键值:
value:
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为 undefined。
复制代码
writable:
当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。
默认为 false。
复制代码
存取描述符同时具有以下可选键值:
get:
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
默认为 undefined。
复制代码
set:
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
默认为 undefined。
复制代码
Object.getOwnPropertyDescriptor()
obj:
需要查找的目标对象
复制代码
prop:
目标对象内属性名称(String类型)
复制代码
descriptor:
将被定义或修改的属性描述符。
复制代码
返回值:
返回值其实就是 Object.defineProperty() 中的那六个在 descriptor
对象中可设置的属性,这里就不废话浪费篇幅了,大家看一眼上面就好
复制代码
defineReactive 的参数我就不一一列举的来讲了,大概从参数名也可以知道大概的意思,具体讲函数内容的时候,在细讲。
Dep
var dep = new Dep();
复制代码
在一进入到 defineReactive 这个函数时,就实例化了一个Dep的构造函数,并把它指向了一个名为dep的变量,下面,我们来看看Dep这个构造函数都做了什么:
var uid = 0;
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Dep.target = null;
复制代码
在实例化 Dep 之前,给 Dep 添加了一个 target 的属性,默认值为 null;
Dep在实例化的时候,声明了一个 id 的属性,每一次实例化Dep的id都是唯一的;
然后声明了一个 subs 的空数组, subs 要做的事情,就是收集所有的依赖;
addSub:
从字面意思,大家也可以看的出来,它就是做了一个添加依赖的动作;
removeSub:
其实就是移除了某一个依赖,只不过实现没有在当前的方法里写,而是调用的一个 remove 的方法:
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
复制代码
这个方法,就是从数组中,移除了某一项;
depend:
添加一个依赖数组项;
notify:
通知每一个数组项,更新每一个方法;
这里 subs 调用了 slice 方法,官方注释是 “ stabilize the subscriber list first ” 字面意思是 “首先稳定订户列表”,这里我不是很清楚,如果知道的大佬,还请指点一下
复制代码
Dep.target 在 Vue 实例化之前一直都是 null ,只有在 Vue 实例化后,实例化了一个 Watcher 的构造函数,在调用 Watcher 的 get 方法的时候,才会改变 Dep.target 不为 null ,由于 Watcher 涉及的内容也很多,所以我准备单拿出一章内容,在 Vue 实例化之后去讲解,现在,我们就暂时当作 Dep.target 不为空。
现在,Dep 构造函数讲解的就差不多了,我们继续接着往下看:
var property = Object.getOwnPropertyDescriptor(obj, key);
复制代码
方法返回指定对象上一个自有属性对应的属性描述符并赋值给property;
if (property && property.configurable === false) {
return
}
复制代码
我们要实现响应式数据的时候,要看当前的 object 上面是否有当前要实现响应式数据的这个属性,如果没有,并且 configurable 为 false,那么就直接退出该方法。
在上面我们介绍过 configurable 这个属性,如果它是 flase ,说明它是不允许被更改的,那么就肯定不支持响应式数据了,那肯定是要退出该方法的。
var getter = property && property.get;
if (!getter && arguments.length === 2) {
val = obj[key];
}
复制代码
获取当前该属性的 get 方法,如果没有该方法,并且只有两个参数(obj 和 key),那么 val 就是直接从这个当前的 obj 里面获取。
var setter = property && property.set;
复制代码
获取当前属性的 set 方法。
var childOb = !shallow && observe(val);
复制代码
判断是否要浅拷贝,如果传的是 false ,那么就是要进行深拷贝,这个时候,就需要把当前的值传递给 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
}
复制代码
在 defineReactive 中,调用 observe 方法,只传了一个参数,所以这里是只有 value 一个值的,第二个值其实就是一个 boolean 值,用来判断是否是根数据;
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
复制代码
首先,要检查当前的值是不是对象,或者说当前的值的原型是否在 VNode 上,那就直接 return 出当前方法, VNode 是一个构造函数,内容比较多,所以这一章暂时不讲,接下来单独写一篇去讲 VNode。
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
复制代码
这里用来判断对象是否具有该属性,并且对象上的该属性原型是否指向的是 Observer ;
如果是,说明这个值是之前存在的,那么变量 ob 就等于当前观察的实例;
如果不是,则是做如下判断:
var shouldObserve = true;
function toggleObserving (value) {
shouldObserve = value;
}
复制代码
shouldObserve 用来判断是否应该观察,默认是观察;
var _isServer;
var isServerRendering = function () {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
// detect presence of vue-server-renderer and avoid
// Webpack shimming the process
_isServer = global['process'] && global['process'].env.VUE_ENV === 'server';
} else {
_isServer = false;
}
}
return _isServer
};
复制代码
是否支持服务端渲染;
Array.isArray(value)
复制代码
当前的值是否是数组;
isPlainObject(value)
复制代码
用来判断是否是Object;具体代码上一篇文章当中有描述,入口在这里:Vue 源码解析 - 实例化 Vue 前(一)
Object.isExtensible(value)
复制代码
判断一个对象是否是可扩展的
value._isVue
复制代码
判断是否可以被观察到,初始化是在 initMixin 方法里初始化的,这里暂时先不做太多的介绍。
这么多判断的总体意思,就是用来判断,当前的值,是否是被观察的,如果没有,那么就创建一个新的出来,并赋值给变量 ob;
asRootData 如果是 true,并且 ob 也存在的话,那么就给 vmCount 加 1;
最后返回一个 ob。
接下来,开始响应式数据的核心代码部分了:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
},
set: function reactiveSetter (newVal) {
}
});
复制代码
首先,要确保要监听的该属性,是可枚举、可修改的的;
get
var value = getter ? getter.call(obj) : val;
复制代码
先前,在前面把当前属性的 get 方法,传给 getter 变量,如果 getter 变量存在,那么就把当前的 getter 的 this 指向当前的 obj 并传给 value 变量;如果不存在,那么就把当前方法接收到的 val 参数传给 value 变量;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
复制代码
每次在 get 的时候,判断 Dep.target 是否为空,如果不为空,那么就去添加一个依赖,调用实例对象 dep 的 depend 方法,这里在 Watcher 的构造函数里,还做了一些特殊处理,等到讲解 Watcher 的时候,我会把这里在带过去一起讲一下。
反正大家记着,在 get 的时候添加了一个依赖就好。
如果是存在子级的话,并且给子级添加一个依赖:
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
复制代码
如果当前的值是数组,那么我们就要给这个数组添加一个监听,因为本身 Array 是不支持 defineProperty 方法的;
所以在这里,作者给所有的数组项,添加了一个依赖,这样每一个数组选项,都有了自己的监听,当它被改变的时候,会根据监听的依赖,去做对应的更新。
set
var value = getter ? getter.call(obj) : val;
复制代码
这里,和 get 时候一样,获取当前的一个值,如果不存在,就返回函数接收到的值;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
复制代码
如果当前值和新的值一样,那就说明没有什么变化,这样就不需要改,直接 return 出去;
如果是在开发环境下,并且存在 customSetter 方法,那么就调用它;
如果当前的属性存在 set 方法,那么就把 set 方法指向 obj,并把 newVal 传过去;
如果不存在,那么就直接把值给覆盖掉;
如果不是浅拷贝的话,那么就把当前的新值传给 observe 方法,去检查是否已经被观察,并且把新的值覆盖到 childOb 上;
最后调用 dep 的 notify 方法去通知所有的依赖进行值的更新。
概括
到这里,基本上 vue 实现的响应式数据的原理,抛析的就差不多了,但是整体涉及的东西比较多,可能看起来会比较费劲一些,这里我概括一下:
- 每次在监听某一个属性时,要先实例化一个队列 Dep,负责监听依赖和通知依赖;
- 确认当前要监听的属性是否存在,并且是可修改的;
- 如果没有接收到参数 val,并且参数只接收到2个,那么就直接把 val 设置成当前的属性的值,不存在就是 undefined;
- 判断当前要监听的值是需要深拷贝还是浅拷贝,如果是深拷贝,那么就去检查当前的值是否被监听,没有被监听,那么就去实例化一个监听对象;
- 在调用 get 方法,获取到当前属性的值,不存在就接收调用该方法时接收到的值;
- 检查当前的队列,要对哪一个 obj 进行变更,如果存在检查的目标的话,那就添加一个依赖;
- 如果存在观察实例的话,在去检查一下当前的值是否是数组,如果是数组的话,那么就做一个数组项的依赖检查;
- 在更新值的时候,发现当前值和要改变的值是相同的,那么就不进行任何操作;
- 如果是开发环境下,还会执行一个回调,该回调实在值改变前但是符合改变条件时执行的;
- 如果当前的属性存在 setter 方法,那么就把当前的值传给 setter 方法,并让当前的 setter 方法的 this 指向当前的 obj,如果不存在,直接用新值覆盖旧值就好;
- 如果是深拷贝的话,就去检查遍当前的值是否被观察,如果没有被观察,就进行观察;(上面大家可能有发现,它已经进行了一次观察,为什么还要执行呢?因为上面是在初始化的时候去观察的,当该值改变以后,比如类型改变,是要进行重新观察,确保如果改变为类似数组的值的时候,还可以进行双向绑定)
- 最后,通知所有添加对该属性进行依赖的位置。
结束语
对应 vue 的响应式数据,到这里就总结完了,未来在实例化 vue 对象的地方,会涉及到很多有关响应式数据的地方,所以建议大家好好看一下这里。
对于源码,我们了解了作者的思想就好,我们不一定要完全按照作者的写法来写,我们要学习的,是他的编程思想,而不是他的写法,其实好多地方我觉得写的不是很合适,但是我不是很明白为什么要这么做,也许是我水平还比较低,没有涉及到,接下来我会对这些疑问点,进行总结,去研究为什么要这么做,如果不合适,我会在 github 中添加 issues 到时候会把链接抛出来,以供大家参考学习。
最后还是老话,点赞,点关注,有问题了,评论区开喷就好