源码链接:Vue2.x数据响应式原理
1.数据响应式原理
1.1什么是数据响应式?
【Vue—非侵入式】
当数据改变后,Vue会通知到使用该数据的代码。数据响应式原理的核心是采用了数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty()对数据进行拦截,把这些属性全部转换为getter/setter,get()方法可以读取数据、收集数据,set()方法可以改写数据,在数据变动时会对数据进行比较,如果数据发生了变化,会发布信息通知订阅者,更新视图。
1.2侦测数据变化的方法—Object.defineProperty()
该方法会直接在一个对象上定义一个新属性,或者修改一个对象现有属性,并返回对象。
功能:将属性转换为getter/setter的形式来追踪变化。
getter:读取数据会触发;收集观察者
setter:修改数据会触发,去通知getter中的依赖数据发生变化;通知观察者进行更新视图或处理事务。
Vue2中利用闭包将Object.defineProperty封装到defineReactive方法中,临时变量也存放在此方法中。
1.3实现defineReactive函数
实现数据劫持, 来实现监听数据的改变和读取(属性的getter和setter)
Object.defineProperty把数据转为getter和setter,并为每个数据添加一个订阅列表的过程。
此函数还需要用到observe函数以及Dep函数
observe:
【用于监听对象嵌套对象】
Dep:
【用于收集依赖】
defineProperty函数执行过程中新建了一个Dep,不需要设置临时变量,而是闭包在了属性的getter和setter中,因此每个属性都有一个唯一的Dep与其对应,当读取响应式对象的某个属性时,它会进行收集依赖。当改变某个属性时,他会派发更新
get: 属性的getter函数。当访问该属性时,会调用此函数。执行时不闯入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
set: 属性的setter函数。当属性值被改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值的this对象,默认为undefined
import observe from "./observe";
import Dep from "./Dep";
// 给对象obj定义一个响应式的属性
// obj: 传入的数据,key:监听的属性,value:闭包环境提供的周转变量
export default function defineReactive (obj, key, val) {
// 每个数据都要维护一个属于自己的数组,用来存放依赖自己的watcher
// 创建信息中心
const dep = new Dep();
// 当前传入参数的个数
if(arguments.length == 2) {
val = obj[key];
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数,类循环调用
let childOb = observe(val);
// 可以使用Object.defineProperty去定义一些隐藏的属性
// 对obj的key进行属性拦截
// val的值相当于get和set这两个函数闭包中的环境
// 闭包是一定要有内外两层函数嵌套,get、set是内层,defineReactive是外层
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
// 是否可写
// writable: true
// get和value不能同时使用
// getter/setter 需要变量周转才能工作
// 临时变量不是特别美观,可以封装到一个函数中,利用函数的闭包特性
// 闭包就是函数外部的作用域
// getter 获取数据
get() {
console.log('访问'+ key + '属性');
// 如果现在处于收集依赖阶段
// Dep.target就是Watcher实例
// 数据变化时,通知添加订阅者
if(Dep.target) {
dep.depend();
// 数组情况,判断有没有子元素
if(childOb) {
// 数组收集依赖
childOb.dep.depend();
}
}
return val;
},
// setter对变量的赋值
set(newValue) {
// 负责劫持
console.log('修改' + key + '属性', newValue);
// 如果传入的值相等就不用修改
if(val == newValue) {
return;
}
// 修改数据
val = newValue;
// 当设置了新值,这个新值也要被observe
// 新值如果是对象,仍然需要递归遍历处理
childOb = observe(newValue);
// 触发依赖
// 发布订阅模式,通知dep执行更新方法
dep.notify();
}
});
}
上述代码用于实现数据劫持,
对象的响应式处理 ↓
对象是在getter中收集依赖,在setter中触发依赖
1.4 递归侦测对象全部属性object
1.4.1 实现observe函数:对象版
用于创建Observer实例
监听value尝试创建Observer实例,如果value已经是响应式数据,就不需要再创建Observer实例,直接返回已经创建的Observer实例即可,避免重复侦测value变化的问题
由于defineProperty函数不能监听到对象嵌套对象,所以我们要创建一个Observer类 —>将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object。
此函数还需要Observer函数
Observer:
【把一个object中的所有数据(包括子数据)都转成响应式,它会侦测object中所有的数据的变化】
import Observer from "./Observer.js";
0
export default function observe(value) {
// 如果value不是对象,什么都不做(表示该递归到的是基本类型,其变化可被侦听的)
if(typeof value != 'object')
{
return;
}
// Observer实例
// 定义ob
let ob;
// 第一步是调observe(obj)来触发全部东西
// 第二步是看obj身上有没有__ob__
// 如果没有就会new Observer()
/*
将产生的实例,添加到__ob__上
*/
// 遍历下一层属性,逐个defineReactive
/*
当设置某个属性值的时候,会触发set,里面有newValue,
这个newValue也得被observe()一下
*/
// __ob__是value上的属性,其值是对应的Observer实例(表示其已经是可侦听的状态)
if(typeof value.__ob__ !== 'undefined') {
// 已经侦听状态
ob = value.__ob__;
} else {
// 是对象且该上属性还是未能够侦听状态
// 此时构造器就会被执行
ob = new Observer(value);
}
return ob;
}
1.4.2 实现Observer类
把一个object中的所有数据(包括子数据)都转成响应式,它会侦测object中所有的数据的变化。
对象的侦测属性还需要Dep函数、def函数、defineReactive函数的辅助【其余的函数需求是用于数组的侦测】
Dep:
【收集依赖】
def:
【给实例添加__ob__属性,值是Observer的实例,且不可枚举】
defineReactive:
【将object的所有数据转成响应式】
- __ob__的作用可以用来标记当前value是否已经被Observer转换为响应式数据了; 而且可以通过value.__ob__来访问Observer的实例
- walk:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object【通过循环遍历属性,使用defineReactive方法将属性变为响应式】
import { def } from './utils.js'
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
// Observer 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
export default class Observer {
// 构造器
constructor(value) {
// 每个Observer的实例,成员中都有一个Dep的实例
// 将__ob__绑在dep上
// 当数据被修改时,就会触发dep
this.dep = new Dep();
// 添加__ob__属性,实际上是不可枚举属性
// 给实例this,一定要注意,构造函数中的this不是表示类本身,而是表示实例),给实例添加了__ob__属性,值是这次new的实例
// 定义一个对象属性
def(value, '__ob__', this, false);
console.log('构造器', value);
// 检查是数组还是对象
if(Array.isArray(value)) {
Object.setPrototypeOf(value, arrayMethods);
this.observeArray(value);
} else {
// 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
this.walk(value);
}
};
// 遍历,遍历value里面的每一个key,让每一个key设置为defineReactive
// 对于对象上的属性进行遍历,将其变为响应式
walk(value) {
// defineReactive被Observer的walk方法调用
for(let k in value) {
// 把value的k属性变成reactive
// 通过这步操作,外层变成响应式
defineReactive(value, k);
}
}
// 数组的特殊遍历
observeArray(arr) {
for(let i = 0; i < arr.length; i++) {
observe(arr[i]);
}
}
}
1.4.3 实现def函数
用于定义一个对象属性
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
});
};
1.5侦测对象全部属性-文件之间的依赖结构
数组的响应式处理 ↓
数组是在getter中收集依赖,在拦截器中触发依赖
1.6 递归侦测数组全部属性
Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容。所以需通过拦截器去覆盖数组原型的方法来追踪变化。由于使用依赖位置不同,数组要在拦截器中向依赖发信息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在Observer实例中。
1.6.1 实现Observer类:数组版
- 在Observer中,我们对每个侦测变化的数据都标记__ob__,并把this(Observer实例)保存在__ob__上
- 作用: 一方面是为了标记数据是否被侦测变化,另一方面方便通过数据取到__ob__,从而拿到Observer实例上保存依赖。当拦截到数组发生变化时,向依赖发送通知。
- 除了侦测数组自身的变化外,数组中的元素发生的变化也要侦测。我们在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一个元素都转换为响应式的并侦测变化。
- Object.setPrototypeOf(obj, prototype):修改对象原型
- obj要设置其原型的对象prototype该对象放入新的原型(一个对象或null)
- Object.create(): 创建一个新对象
- 该方法创建一个新的对象,使用现有的对象来提供新创建的对象的__proto__
数组的侦测属性还需要observe函数以及arrayMethods函数的辅助
observe
【用于监听对象嵌套对象】
arrayMethods
【 自定义方法去覆盖原生的原型方法】
import { def } from './utils.js'
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
// Observer 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
export default class Observer {
// 构造器
// 类需要思考如何被实例化
constructor(value) {
// 每个Observer的实例,成员中都有一个Dep的实例
// 将__ob__绑在dep上
// 当数据被修改时,就会触发dep
this.dep = new Dep();
// 添加__ob__属性,实际上是不可枚举属性
// 给实例this,一定要注意,构造函数中的this不是表示类本身,而是表示实例),给实例添加了__ob__属性,值是这次new的实例
// 定义一个对象属性
def(value, '__ob__', this, false);
console.log('构造器', value);
// 检查是数组还是对象
if(Array.isArray(value)) {
// 如果是数组,要强行将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// 让数组变得observe
this.observeArray(value);
} else {
this.walk(value);
}
};
walk(value) {
defineReactive(value, k);
}
}
// 数组的特殊遍历
observeArray(arr) {
for(let i = 0; i < arr.length; i++) {
/*
* Observer类会附加到每一个被侦测的object上,一旦被附加,Observer会将object所有属性转换为getter/setter的形式
*/
// 逐项进行observe
observe(arr[i]);
}
}
}
1.6.2 实现ArrayMethods函数
改写数组的七个方法(可以改变数组自身内容的方法),在保留原有功能的前提下,将其添加的项变为响应式,来实现对数组的响应式处理。
当用户使用push、unshift、splice方法向树祖中新增数据时,新增数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push、unshift、splice,则从参数中将新增数据提取出来,然后使用observeArray来新增数据进行变化侦测。
1.以arrayPrototype为原型(proto)创建一个arrayMethods对象,并将其暴露;
Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
2.用ES6强制定义数组的原型指向arrayMethods:Object.setPrototypeOf(o, arrayMethods) / Object.create() / o.__proto = arrayMethods要被改写的7个数组方法,Vue通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理
Object.defineProperty不能直接监听数组内部的变化,那么数组内容变化应该怎么操作?
Vue主要采用的是改装数组方法的方式(push,pop,shift,unshift,splice,sort,reverse), 在保留其原有功能的前提下,将其添加的项变为响应式
3.相当于一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。
此函数还需要用到def函数
def
【用于定义新的方法】
import { def } from './utils.js';
/* 正因为可以通过Array原型上的方法来改变数组的内容,所以像对象那种通过getter/setter的实现方式就行不通
* ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法
*/
// 得到Array的原型
const arrayPrototype = Array.prototype;
// 这七个方法定义在Array.prototype上,要保留方法的功能,同时增加数据劫持
console.log(arrayPrototype);
export const arrayMethods = Object.create(arrayPrototype);
const methodsNeedChange = [
// // Vue底层改写这7个方法
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
// 备份原来的方法
const original = arrayPrototype[methodName];
// 定义新的方法
// 对象,名字,定义什么值
def(arrayMethods, methodName, function() {
// 恢复原来的功能
const result = original.apply(this, arguments);
// 把类数组对象变为数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?
/*
* 因为数组不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层
的时候,已经给g属性(就是这个数组)添加了__ob__属性
*/
// 数组不会是最外层,所以其上已经添加了Observer实例
// 把这个数组身上的__ob__取出来
// 在拦截器中获取Observer的实例
const ob = this.__ob__;
// 有三种方法push/unshift/splice能够插入新项,需把插入的新项也要变为observe的
let inserted = [];
switch(methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
inserted = args.slice(2);
}
// 查看有没有新插入的项inserted,有的话就劫持
if(inserted) {
ob.observeArray(inserted);
}
console.log('被修改啦');
// 发布订阅模式,通知dep
// 数据发生了变化,因此需向依赖发送信息
ob.dep.notify();
return result;
}, false);
});
1.7 侦测数组全部属性-文件之间的依赖结构
2.发布订阅模式
发布-订阅是对象的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。在Vue2.x中数据响应式中,也是一个发布订阅模式
发布者:通过实践中心派发事件—observe
- 执行了发布者的权利,增加了订阅者,并且改变时通知订阅者。
事件中心:负责存放事件和订阅者的关系—Dep
- 管理真实订阅者Wacther
订阅者:通过事件中心进行事件的订阅—Watcher
- 通过触发get,将watcher添加到key对应的Dep中
2.1收集依赖-Dep,依赖-Watcher
2.1.1为什么要收集依赖?
之所以要劫持数据,目的是当数据的属性发生变化时,可以通过哪些曾经用到的该数据的地方。
先收集依赖,把用到的数据的地方收集起来,等属性改变,在之前收集好的依赖中循环触发一遍。
总结:对象是在getter中收集依赖,在setter中触发依赖;而数组在getter中收集依赖,在拦截器中触发依赖
2.1.2如何收集依赖?
收集依赖到哪里? Dep
我们要在getter中收集依赖,将依赖收集封装成一个类Dep,这个类帮我们管理依赖可以进行收集依赖、删除依赖、向依赖发送通知。只有Watcher触发的getter才会进行收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher都通知一遍。
let uid = 0;
// 定义依赖关系,收集依赖的核心
export default class Dep {
constructor() {
console.log('我是Dep的构造器');
this.id = uid++;
// 用数组存储自己的订阅者,放的是Watcher的实例
this.subs = [];
}
// 添加订阅: 该方法将订阅者添加到subs中对应的数组中
addSub(sub) {
// sub:订阅者,当信息发生时被通知的对象
this.subs.push(sub);
}
// 添加依赖
// 将dep实例添加到当前的订阅者中(这个过程中也会将当前的订阅者添加到dep的订阅者列表中)
depend() {
// target是一个全局静态属性,可以理解为当前的目标Watcher,也就是当前的订阅者
// Dep.target=>类的属性,并不是Dep实例的属性,就是一个我们自己指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行
if(Dep.target) {
// 在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep
// Dep.target就是Watcher实例
// Why 为什么要存放在Dep.target?
/* 举例: {{a+b}}
* 因为getter函数并不能传参,dep可以通过闭包的形式放进去,watcher不行,watcher内部存放了a+b这个表达式,
* 也是由watcher计算a+b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a+b,从而触发
* 表达式中所有遇到的依赖getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式
* 计算成功,Dep.target又恢复null。这样就成功的让watcher分发到了对应依赖的订阅者列表中,订阅到自己所有依赖。
*/
this.addSub(Dep.target);
}
}
// 通知所有的订阅者进行更新
notify() {
console.log('Notify');
// 浅克隆一份
const subs = this.subs.slice();
// 遍历
for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
// 从数组中删除元素item
remove(arr, item) {
if(arr.length) {
const index = arr.indexOf(item);
if(index > -1) {
return arr.splice(index, 1);
}
}
}
}
依赖是什么? Watcher
Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
import Dep from "./Dep";
/*
*功能:
实例化Watch时,往dep中添加自己
当数据变化触发dep,dep通知所有对应的Watch实例更新视图
*/
let uid = 0;
// Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
export default class Watcher {
constructor(target, expression, callback) {
// target 监听哪个对象实例,expression哪个对象怎样的表达式【订阅的属性名】,callback数据变化后要执行的回调
console.log("我是watcher类的构造器");
this.id = uid++;
// 触发getter前,将当前订阅者实例村春给Dep类
this.target = target;
// 把表达式按点来拆分,执行this.getter()就可以读取data.a.b.c的内容
this.getter = parsePath(expression);
this.callback = callback;
// 记录属性更改之前的值,用于进行更新状态检测(导致属性的getter的触发)
this.value = this.get();
}
update() {
this.run();
}
get() {
// 进入依赖收集阶段,让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this;
const obj = this.target;
var value;
// 只要能找到,就一直找
try {
value = this.getter(obj);
} finally {
// 操作完毕后清除target,用于存储下一个Watch实例
Dep.target = null;
}
return value;
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(cb) {
const value = this.get();
if(value !== this.value || typeof value == 'object') {
const oldValue = this.value;
this.value = value;
// 调用Watcher实例的时候传递过来的回调函数,并且确定它的this指向this.target
cb.call(this.target, value, oldValue);
}
}
};
// 将str用.分割成数组segments,然后循环数组,一层一层去读取数据,最后拿到的obj就是str中想要读的数据
function parsePath(str) {
var segments = str.split('.');
return(obj) => {
for(var i = 0; i < segments.length; i++) {
if(!obj) return;
obj = obj[segments[i]];
}
return obj;
}
}
2.2应用场景
- DOM操作中的addEventListener
- Vue中的事件总线的概念
- Node.js中的EventEmitter以及内置库
2.3优点
- 模块间进行耦合,不强关联与特定的其他模块,只需订阅相关事件即可
- 异步编程中,代码可以松耦合。
2.4缺点
- 松耦合弱化对象间的关系,debug时程序可能难以追踪