前言
响应式是指当数据发生变化后,Vue会通知到使用该数据的代码。当数据发生改变后也会通知视图进行改变,下面我们将简单的实现一下Vue中响应式原理的核心代码部分。
源码解析
Observer
Observer类的目的是将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object。
实现部分:
export default class Observer {
constructor(value) {
// 每一个Observer的实例身上,都有一个dep
this.dep = new Dep();
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
// 对__ob__每个属性使其无法进行枚举遍历,def这里是给予这个value添加__ob__并且无法进行枚举,
// 并且其他属性也无法被修改
// 这个地方使得当前元素的__ob__是这个Observer类
def(value, '__ob__', this, false);
// console.log('我是Observer构造器', value);
// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods
// 修改其对应的原型为arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// 让这个数组变的observe
this.observeArray(value);
} else {
// 如果是对象的话则挨个的去进行遍历对象的每个属性
this.walk(value);
}
}
// 遍历
walk(value) {
// 对每个对象进行设置响应式
for (let k in value) {
defineReactive(value, k);
}
}
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i]);
}
}
};
const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
});
};
1. 首先先对所传入Observer的对象设置一个属性为__ob__,这也就是我们平时在Vue打印输出变量对象时,可以看到其对象中是存在__ob__属性的,那么这个属性实际就是Observer实例。在代码中我们是通过def函数来去定义__ob__属性的,给予其进行劫持且enumerable为false,不可进行枚举。
2.判断所传入的对象是数组还是对象:
如果是数组的话调用observeArray依次遍历数组的每一项都进行observe(这个方法我们在下面介绍,实际就是给数组的每一项都添加上Observer实例),对于数组来讲,我们还需要改写原型,对数组对象的七个方法进行改写,从而监听插入插入方法中所插入的数据,并对插入的数据进行数据监听,改变其数组对象的原型,设置为arrayMethods,此时arrayMethods是我们根据数组原型所进行改写的原型对象。
如果是对象的话,则调用walk方法依次遍历对象的每一个值,并把每一个值都设置为响应式,这里调用了defineReactive方法,我们在后续进行详解。
那么在Observer中所涉及的几个方法我们在下面进行赘述:
arrayMethods
arrayMethods是改写了数组内部的方法,主要是监听进行插入元素的数组方法,并对所插入的元素使其也变为响应式的。对于非插入元素的数组方法,我们还是恢复其原来的功能即可,其实现代码如下:
// 得到Array.prototype
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
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__属性。
const ob = this.__ob__;
// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = [];
// 对插入新项的方法也要编程observe类型的
switch (methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
// slice所截取的是从下标为2一直往后,所以能够获取插入的新项
inserted = args.slice(2);
break;
}
// 判断有没有要插入的新项,让新项也变为响应的
if (inserted) {
ob.observeArray(inserted);
}
return result;
}, false);
});
分析代码可得,首先先由数组对象的原型创建出了新的原型对象,然后我们列出数组的方法,并对这些方法进行重写,通过switch当遍历到到push、unshift、splice函数,并且splice函数所传入的参数为3个时(说明为插入项),则将inserted赋值为我们所插入的元素,如果判断inserted数组不为空,则重新遍历inserted的数组元素,并将其每一项都设置为响应式的。
observe
observe将其传入的对象进行通过Observer类进行实例化,那么通过观察之前Observe类的代码就可以发现,这里会存在递归调用,使得每一个子对象都进行Observer化。例如在Observer中当前对象为数组时会调用observer方法,在observer中发现其为对象又会调用Observer类进行实例化,这样就会使得每一个子对象都被observer化。
import Observer from './Observer.js';
// value是
export default function (value) {
// 如果value不是对象,什么都不做
if (typeof value != 'object') return;
// 定义ob
var ob;
// __ob__是存储Observer实例的
// 如果value身上有__ob__的话则直接赋值为ob并进行返回
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
} else {
// 如果没有则进行初始化进行返回
ob = new Observer(value);
}
return ob;
}
defineReactive
为对象的属性添加响应式,进行数据劫持,我们还可以通过getter与setter来去进行一些操作,在后面我们会讲解到,在getter的时候就进行收集依赖,在setter函数里面我们就调用notify函数去通知dep进行进行更新,代码如下:
export default function defineReactive(data, key, val) {
// 创建dep实例
const dep = new Dep();
// console.log('我是defineReactive', key);
if (arguments.length == 2) {
val = data[key];
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val);
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
// configurable特性表示对象的属性是否可以被删除,
// 以及除value和writable特性外的其他特性是否可以被修改。
configurable: true,
// getter
get() {
console.log('你试图访问' + key + '属性');
// 如果现在处于依赖收集阶段
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();
}
});
};
Dep
Dep是链接Observer和Watcher的桥梁,每一个Observer对应一个Dep,他内部维护一个数组,存放与该Obverser相关的Watcher
对应实现代码:
// 闭包
var uid = 0;
// Dep是链接Observer和Watcher的桥梁,每一个Observer对应一个Dep,他内部维护一个数组,保护
// 与该Obverser相关的Watcher
export default class Dep {
constructor() {
console.log('我是DEP类的构造器');
// 每创建一个就创建一个唯一的ID值
this.id = uid++;
// 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
// 这个数组里面放的是Watcher的实例
// Watcher是我们所说的订阅者
this.subs = [];
}
// 添加订阅
addSub(sub) {
// 将sub推入数组中
this.subs.push(sub);
}
// 收集依赖
// 添加依赖
depend() {
// Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
if (Dep.target) {
// 就把相对应的watcher添加进来
this.addSub(Dep.target);
}
}
// 通知更新
notify() {
console.log('我是notify');
// 浅克隆一份
const subs = this.subs.slice();
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
// 执行watcher的update函数,使其调用getAndInvoke,执行对应的回调函数进行更新操作
subs[i].update();
}
}
};
对于Dep它是链接Observer和Watcher的桥梁,当进行依赖收集时,我们就把对应的订阅者添加到Dep对象中,当Observer触发setter时,如果新值与旧值不同,那么则调用Dep实例的notify方法,通知实例中所有的订阅者(Watcher)进行调用update方法,通知组件进行更新。
Watcher
Watcher扮演的角色是订阅者/观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。
下面我们贴出实现代码:
import Dep from "./Dep";
var uid = 0;
// Watcher是一个订阅者身份,当监听的数据值修改时,
// 执行响应的回调函数,在Vue里面的更新模板内容
export default class Watcher {
// target代表需要监听哪个对象
// expression代表表达式,paresePath可以将其按.进行拆分
// callback是代表其回调函数
constructor(target, expression, callback) {
console.log('我是Watcher类的构造器');
this.id = uid++;
this.target = target;
this.getter = parsePath(expression);
this.callback = callback;
// 获取目标对象的value值
this.value = this.get();
}
update() {
this.run();
}
get() {
// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this;
// 获取传进来的目标对象
const obj = this.target;
var value;
// 只要能找,就一直找
try {
// 获取对象根部的value值
value = this.getter(obj);
} finally {
// 将全局Dep对象为null
Dep.target = null;
}
return value;
}
run() {
this.getAndInvoke(this.callback);
}
//得到并且唤起
getAndInvoke(cb) {
// 获取目标对象的value值
const value = this.get();
// 如果是值类型,如果之前的值不等于当前的值则执行
// 如果为对象类型也执行对应的回调函数
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value;
this.value = value;
// this指向设置为这个传入的对象,传参为newValue与oldValue
// 执行更新操作
cb.call(this.target, value, oldValue);
}
}
};
function parsePath(str) {
// 通过.来进行解析
var segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
// 比如传入 a.b.c.d
// 这一步能逐渐剥得a:{b:{c:{d:55}}},下一步是b:{c:{d:55}},然后是c:{d:55}等等,直到可以获取目标值
obj = obj[segments[i]]
}
return obj;
};
}
由代码分析可得,在Watcher类中expression代表我们逐步访问对象的层级属性值例如:“a.b.c.d”,代表我们需要访问a对象下面的d子对象,这个通过parsePath函数来进行解析,在调用Watcher起初我们就调用了get方法,get方法使得Dep.target设置为当前订阅者,然后调用了getter方法,当调用getter方法时,此时会触发defineReactive函数中defineProperty的数据劫持会使得触发get函数,在get函数中会判断Dep.target(某一个观察者)是否存在,如果存在的话则进行依赖收集,因为每一个被监听的数据都有一个Dep实例,那么将会把对应的Watcher添加到被监听数据(Observer)的Dep实例对象中进行依赖收集,一个数据会由多个订阅者进行订阅,统一放在Dep列表中。
总结
Observe用于对数据进行监听,Dep是一个订阅器,存储多个订阅者,并通知实例中存储的订阅者进行更新。Watcher是订阅者/观察者,被Observe中Dep的实例对象进行存储,Observe中数据发生变化时,进行通知组件进行更新。
当监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明当前观察者(Watcher)是依赖该数据的,此时会把当前的观察者添加到Observer中Dep实例的subs数组中,等待后续数据变更的通知
当监听的数据进行赋值操作时(setter),此时会触发Observer中Dep实例对象的notify方法,用于通知Dep实例对象中所有订阅者进行更新(通知组件进行更新)。
下面我们放一张图例,能够更好的说明三者之间的关系。
本次分享的文章就到此结束啦,欢迎在讨论区一起交流~
Vue源码系列文章: