由于项目变更,技术栈,需要从React转vue,于是决定将学习vue的过程记录下来,加深对框架的理解
前提
如果你对Vue原理或者源码比较了解,可以直接跳过
目标
本章节的目标,彻底弄懂vue的响应式是如何实现的,Observer、Dep、Watcher之间的关系是什么
如何追踪对象的变化
这里的对象,是指Plain Object,下文会提到数组如何监测变化。看过Vue官方文档的话,我们都知道,vue 3.x以前,是通过Object.defineProperty将数据对象变成响应式。定义函数defineReactive
/**
* 将对象的属性变成响应式
* @param {object} data
* @param {string} key
* @param {any} val
*/
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return val;
},
set(newVal) {
val = newVal;
}
});
}
每当访问data[key]时,get函数都会被触发一次,每当修改data[key]的值时,set函数都会被触发一次,我们可以在get函数里收集所有的依赖,然后在set函数里通知这些依赖。
依赖收集怎么做
我们先假设依赖是一个函数,保存在window.target上。此时,defineReactive函数修改如下:
/**
* 将数据转换成响应式数据
* @param {object} data
* @param {string} key
* @param {any} val
*/
function defineReactive(data, key, val) {
// 新增代码
const dep = [];
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 新增代码
dep.push(window.target);
return val;
},
set(newVal) {
val = newVal;
// 新增代码
for (let i = 0; i < dep.length; i++) {
dep[i](newVal, val);
}
},
});
}
在上面这段代码里,新增了dep,存放所有依赖此属性的相关函数,在get函数中push到dep数组中,在set函数里触发所有的函数。为了使代码复用,我们将dep相关代码抽离成单独的类,如下所示:
/**
* 定义Dep类
*/
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (window.target) {
this.addSub(window.target);
}
}
notify(newValue, value) {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i](newValue, value);
}
}
}
/**
* 将数据转换成响应式数据
* @param {object} data
* @param {string} key
* @param {any} val
*/
function defineReactive(data, key, val) {
// 改动
// const dep = [];
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 改动
// dep.push(window.target);
dep.depend();
return val;
},
set(newVal) {
val = newVal;
// 改动
// for (let i = 0; i < dep.length; i++) {
// dep[i](newVal, val);
// }
dep.notify(newVal, val);
},
});
}
window.target是什么
这里的window.target代表了多种读取数据的函数,比如模板渲染函数,比如选项watch里面的每一项,我们把这种函数或者对象,统称为Watcher,下面以$watch实例方法举例说明
$watch背后的逻辑
比如下面这段代码:
vm.$watch('a.b', function (newValue, oldValue) {
// do something
});
/**
* 先简单定义一下,只解析以.相连的字符串
* @param {string} path
*/
function parsePath(path) {
const segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return;
} else {
obj = obj[segments[i]];
}
}
return obj;
};
}
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
const value = this.getter(this.vm);
window.target = undefined;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
/**
* 定义Dep类
*/
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (window.target) {
this.addSub(window.target);
}
}
notify(newValue, value) {
for (let i = 0; i < this.subs.length; i++) {
// 改动
// this.subs[i](newValue, value);
this.subs[i].update();
}
}
}
上面这段代码可以看出,当执行$watch时,就是实例化了一个Watcher,在Watcher对象的构造函数里对数据进行了读取,读取之前将自身赋值给window.target,这样的话,dep.depend就将该watcher对象加入到了依赖数组中。值读取完毕后,将window.target设置为undefined。当a.b的值发生变更时,dep.notify就会通知到该watcher对象,即执行它的update方法,在watch对象的update方法内部,会再次读取到最新的值,并最后执行回调函数this.cb。
递归所有的key
我们将一个数据内所有的属性变成响应式的的过程,抽离成一个单独的类,称之为Observer
class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]]);
}
}
}
/**
* 将数据转换成响应式数据
* @param {object} data
* @param {string} key
* @param {any} val
*/
function defineReactive(data, key, val) {
// 递归子属性
if (typeof val === 'object') {
new Observer(val);
}
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
dep.depend();
return val;
},
set(newVal) {
val = newVal;
dep.notify(newVal, val);
},
});
}
Array的变化监测
前面的代码,针对的是object的变化监测,下面将讲解如何实现Array的变化监测。
假如我们往数组中新增元素
this.todoList.push({
text: '喝3杯水'
});
如果不对数组做特殊处理,上面这段代码并不会触发setter,因为并未改变this.todoList的引用,唯一的方法就是拦截array常见的api(push、pop、shift、unshift等),在调用原始api之前做些处理,达到监测的目的。于是,我们创建一个和Array.prototype一样的对象
const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
'push',
'pop',
'splice',
'shift',
'unshift',
'sort',
'reverse',
];
methods.forEach((method) => {
const original = originProto[method];
Object.defineProperty(newArrayProto, method, {
value: function (...args) {
return original.apply(this, args);
},
writable: true,
enumerable: false,
configurable: true,
});
});
class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
} else {
// 新增
value.__proto__ = newArrayProto;
}
}
walk(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]]);
}
}
}
针对value是数组的情况,在收集依赖时,需要做些改动。为了能在newArrayProto的每个属性里访问到依赖数组,需要将依赖数组挂在数组上。
function observe(value) {
if (typeof value !== 'object') {
return;
}
if (value.__ob__ && value.__ob__ instanceof Observer) {
return value.__ob__;
} else {
return new Observer(value);
}
}
/**
* 将数据转换成响应式数据
* @param {object} data
* @param {string} key
* @param {any} val
*/
function defineReactive(data, key, val) {
// if (typeof val === 'object') {
// new Observer(val);
// }
const childOb = observe(val);
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
dep.depend();
// 改动
if (childOb) {
childOb.dep.depend();
}
return val;
},
set(newVal) {
val = newVal;
dep.notify(newVal, val);
},
});
}
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
writable: true,
value: val,
enumerable: !!enumerable,
configurable: true,
});
}
class Observer {
constructor(value) {
// 改动
this.dep = new Dep();
def(value, '__ob__', this);
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
} else {
// 新增
value.__proto__ = newArrayProto;
}
}
walk(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]]);
}
}
}
const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
'push',
'pop',
'splice',
'shift',
'unshift',
'sort',
'reverse',
];
methods.forEach((method) => {
const original = originProto[method];
Object.defineProperty(newArrayProto, method, {
value: function (...args) {
// 改动
const ob = this.__ob__;
const result = original.apply(this, args);
// 改动
ob.dep.notify();
return result;
},
writable: true,
enumerable: false,
configurable: true,
});
});
通过以上代码,当对数组进行push、pop、splice、shift、unshift、sort、reverse等操作时,可以监测到数组的变化并作出响应。
除此之外,当数组里每个数组项发生变化时,也需要监测。通过修改Observer代码,将数组项进行响应式化
class Observer {
constructor(value) {
// 改动
this.dep = new Dep();
def(value, '__ob__', this);
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
} else {
// 新增
value.__proto__ = newArrayProto;
this.observeArray(value);
}
}
walk(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]]);
}
}
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]);
}
}
}
另外,对于数组新增的元素,需要将它们也响应式化,于是增加如下代码:
const originProto = Array.prototype;
const newArrayProto = Object.create(originProto);
const methods = [
'push',
'pop',
'splice',
'shift',
'unshift',
'sort',
'reverse',
];
methods.forEach((method) => {
const original = originProto[method];
Object.defineProperty(newArrayProto, method, {
value: function (...args) {
const ob = this.__ob__;
const result = original.apply(this, args);
ob.dep.notify();
// 改动
let newItems;
switch (method) {
case 'push':
case 'unshift': {
newItems = args;
break;
}
case 'splice': {
newItems = args.slice(2);
break;
}
}
if (newItems) {
ob.observeArray(newItems);
}
return result;
},
writable: true,
enumerable: false,
configurable: true,
});
});