最近在学习Vue,学习过Vue的小伙伴应该都会知道Vue在数据发生变化之后就会重新生成虚拟DOM,然后进行新旧虚拟DOM的对比,然后进行替换,页面就进而也就发生了改变。
那么Vue是怎么监测到数据变化呢,Vue底层也是基于Object.defineProperty这个api进行监听数据的变化的,不懂的小伙伴可以百度学习一下这个api,这个api可以监听某个对象某个属性值的变化。
但是数组对象是以下标的形式存取值的,那么想监测数组里某个下标的值的改变,就只能使用设计模式代理的思想,设计一个数组的代理对象,这个代理对象重写能改变原数组元素的方法,例如Array类中的这几个方法
- push() //在数组尾部添加一个元素
- pop() //删除数组的最后一个元素
- shift() //删除数组第一个元素的
- unshift() //在数组前面插入一个元素
- splice() //删除数组指定范围的元素
- sort() //对原数组进行排序
- reverse() //对原数组进行反转
在代理对象中的这些方法又调用父类Array的这些方法,并且可以加入了数据监听的功能。即实现了监听的功能,又不改变功能。下面这个是Array的代理类。
/**
* 创建一个Array的代理类,后续的数组创建都使用这个代理类,才能实现数组值的监听
*/
class ArrayProxy extends Array {
constructor(arraySize) {
if (arraySize === undefined) {
super();
} else {
super(arraySize);
}
}
push(...items) {
let newLength = super.push(...items);
console.log("监测到了执行push方法")
return newLength;
}
pop() {
let popValue = super.pop();
console.log("监测到了执行pop方法");
return popValue;
}
shift() {
let shiftValue = super.shift();
console.log("监测到了执行shift方法");
return shiftValue;
}
unshift(...items) {
let newLength = super.unshift(...items);
console.log("监测到了执行unshift方法");
return newLength;
}
splice(start, deleteCount, ...items) {
let removeValueArray = super.splice(start, deleteCount, ...items);
console.log("监测到了执行splice方法");
return removeValueArray;
}
sort(compareFunc) {
super.sort(compareFunc);
console.log("监测到了执行sort方法");
}
reverse() {
let reversedArray = super.reverse();
console.log("监测到了执行reverse方法");
return reversedArray;
}
}
下面就是数据代理类的实现,取名为Observer,实例化传入的是一个键值对的对象。里面主要是递归的判断每个子对象的类型,根据不同的类型(主要是对象、数组、普通类型),然后又给每个层级的数据设置他们的数据代理,主要依靠的就是Object.defineProperty这个api。
还实现了一个$set(key,value)方法,用于为已经创建好的数据代理对象注入新的属性以及他的值,这样注入的新属性也会被监听值的变化,下面是数据代理类Observer代码实现:
/**
* 数据代理类
*/
class Observer {
dataObj = null;
constructor(dataObj) {
this.dataObj = dataObj;
const keys = Object.keys(dataObj);
// 生成数据对象的数据代理
this.generateListening(this, dataObj, keys);
}
/**
* 动态的为数据代理对象添加新的数据,通过这个函数添加的数据也是可监测的
* @param key 添加的属性名
* @param value 这个属性的值
*/
$set(target, key, value) {
const obj = {
[key]: value
};
const keys = Object.keys(obj);
this.generateListening(target, obj, keys);
}
/**
* 生成obj的数据代理
* @param parent 数据代理的父级
* @param obj 源数据对象
* @param keys 数据对象的key数组
*/
generateListening(parent, obj, keys) {
// 遍历obj对象的键,然后判断这个子元素的类型做对应处理
keys.forEach((key) => {
// 是object类型但不是数组
if (typeof obj[key] === 'object' && !(obj[key] instanceof Array)) {
// 创建数据代理
obj[key] = new Observer(obj[key]);
Object.defineProperty(parent, key, {
get() {
return obj[key];
},
set(newValue) {
console.log(key + '对象整体被赋值了')
obj[key] = new Observer(newValue);
}
})
} else if (typeof obj[key] === 'object' && (obj[key] instanceof Array)) {
// 子对象是数组
let arr = new ArrayProxy();
obj[key].forEach((e, i) => {
// 对每个数组的元素进行判断
if (typeof obj[key][i] === 'object' && !(obj[key][i] instanceof Array)) {
// 数组元素是对象时
arr.push(new Observer(obj[key][i]));
} else if (typeof obj[key][i] === 'object' && (obj[key][i] instanceof Array)) {
// 数组元素还是一个数组时
let childArr = new ArrayProxy();
let childArrKeys = Object.keys(obj[key][i])
// 递归生成这个子数组的所有子孙的数据代理
this.generateListening(childArr, obj[key][i], childArrKeys);
arr.push(childArr);
} else {
// 数组元素是普通的数据类型时
arr.push(e);
}
}
);
// 把生成好的数据代理重新赋值给子元素
obj[key] = arr;
Object.defineProperty(parent, key, {
get() {
return obj[key];
},
set(newValue) {
console.log("数组" + key + "被整体赋值了");
const newValueKeys = Object.keys(newValue);
// 重新根据新数组生成数据代理
this.generateListening(obj[key], newValue, newValueKeys);
}
})
} else {
if (parent instanceof Array) {
// 如果是数组里面的普通元素,直接push就可以
parent.push(obj[key]);
} else {
// 不是数组里面的普通元素,需要设置监听
Object.defineProperty(parent, key, {
get() {
return obj[key];
},
set(newValue) {
console.log(key + "被重新赋值了");
obj[key] = newValue;
}
});
}
}
}
);
}
}
下面模拟一个实例Vue的data数据对象,来使用Observer创建他的数据代理
// 准备数据,模拟创建Vue实例的data
const data = {
schoolName: "云南民族大学",
schoolAddress: "九龙池",
numberPeople: 7000,
library: {
name: "学校图书馆",
},
classes: [
{
className: "1班", teacher: "张三",
students: [
{studentName: "学生1", hobby: ["吃饭", "睡觉", "打游戏"]},
{studentName: "学生2", hobby: ["看书", "写作业", "跑步"]},
]
},
{className: "2班", teacher: "李四"},
{className: "3班", teacher: "王五"},
],
courses: ["计算机网络", "Java程序设计", "数据结构", "数据库设计"],
a: [
[1, 2, 3],
[4, 5, 6],
[{a: 9}, {b: 10}]
]
}
// 创建数据代理
let obs = _data = new Observer(data);
console.log(obs);
下面用浏览器的代码片段工具Script snippet执行上面的全部代码,查看控控制台打印的obj对象。可以看到第一层的属性都有对应的get和set方法
接下来看一下底层的对象有没有对应的get和set,可以从下面的图片看出来下面几级的属性都有对应的get和set方法的
接下来我们来更新library属性的值,就可以看到已经监测到属性值的变化了
我们使用$set方法来为library属性添加新属性,看这个属性能不能被监听
从图片可以看到,通过$set方法添加的属性也是可以监听的。