上一篇文章,大概的讲解了Vue实例化前的一些配置,若是没有看到上一篇,通道在这里:Vue 源码解析 - 实例化 Vue 前(一)javascript
在上一篇的结尾,我说这一篇后着重讲一下 defineReactive 这个方法,这个方法,其实就是你们能够在外面看见一些文章对 vue 实现响应式数据原理的过程。前端
在这里,根据源码,我决定在给你们讲一遍,看看和你们平时本身看的,有没有区别,若是有遗漏的点,欢迎评论vue
先来一段 defineReactive 的源码:java
//在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() react
虽然不少前端的大佬知道它的做用,可是我相信仍是有一些朋友是不认识的,我但愿我写的文章,不仅是传达vue内部实现的一些精神,更能帮助一些小白去了解一些原生的api。git
在 MDN 上的解释是:github
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
复制代码
这里,其实就是用来实现响应式数据的核心之一,主要作的事情就是数据的更新, Object.defineProperty() 最多接收三个参数:obj , prop , descriptor:编程
obj:api
要在其上定义属性的对象。
复制代码
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。
复制代码
obj:
须要查找的目标对象
复制代码
prop:
目标对象内属性名称(String类型)
复制代码
descriptor:
将被定义或修改的属性描述符。
复制代码
返回值:
返回值其实就是 Object.defineProperty() 中的那六个在 descriptor
对象中可设置的属性,这里就不废话浪费篇幅了,你们看一眼上面就好
复制代码
defineReactive 的参数我就不一一列举的来说了,大概从参数名也能够知道大概的意思,具体讲函数内容的时候,在细讲。
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 的方法:
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) {
}
});
复制代码
首先,要确保要监听的该属性,是可枚举、可修改的的;
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 方法的;
因此在这里,做者给全部的数组项,添加了一个依赖,这样每个数组选项,都有了本身的监听,当它被改变的时候,会根据监听的依赖,去作对应的更新。
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 实现的响应式数据的原理,抛析的就差很少了,可是总体涉及的东西比较多,可能看起来会比较费劲一些,这里我归纳一下:
对应 vue 的响应式数据,到这里就总结完了,将来在实例化 vue 对象的地方,会涉及到不少有关响应式数据的地方,因此建议你们好好看一下这里。
对于源码,咱们了解了做者的思想就好,咱们不必定要彻底按照做者的写法来写,咱们要学习的,是他的编程思想,而不是他的写法,其实好多地方我以为写的不是很合适,可是我不是很明白为何要这么作,也许是我水平还比较低,没有涉及到,接下来我会对这些疑问点,进行总结,去研究为何要这么作,若是不合适,我会在 github 中添加 issues 到时候会把连接抛出来,以供你们参考学习。
最后仍是老话,点赞,点关注,有问题了,评论区开喷就好