# 框架原理
双向数据绑定的直观形式无非是,数据在变化时不再需要开发者手动的去更新视图,而是由视图根据数据的变化“自动”更新。完成上述流程只需要做到以下几点即可:
- 当前页面视图需要知道依赖了哪些数据。
- 感知被依赖的数据的变化情况。
- 数据变化时,自动的“通知”需要更新的视图部分,进行视图更新。
而做到上述几点对应的技术概念如下:
- 数据依赖的收集
- 被依赖的数据进行劫持和代理
- 形成发布/订阅的模式。
# 数据的劫持和代理
感知数据变化的方法很直接,对要感知的数据进行劫持和代理。也就是常说的给数据定义 getter 和 setter。定义的方法就是通过 Object.defineProperty。
let data = {
stage: "来了来了",
course: {
title: "前端开发",
author: "lucy",
publishTime: "2022年7月23日",
},
};
// 使用 Object.defineProperty 对数据进行劫持代理
Object.keys(data).forEach((key) => {
let currentVal = data[key];
Object.defineProperty(data, key, {
enumerable: true, // 可枚举的
configurable: false, // 可配置的
get() {
console.log(
`getting ${key} value now, value is ${JSON.stringify(currentVal)}`
);
return currentVal;
},
set(newValue) {
currentVal = newValue;
console.log(`setting ${key} value now, setting value is ${newValue}`);
},
});
});
// 最后要执行一次,才会走 get 方法
data.course;
// getting course value now, value is {"title":"前端开发","author":"lucy","publishTime":"2022年7月23日"}
data.course.title = "修改title触发set"; // 没办法触发 set
上述方法的缺点是,如果修改对象 data.course 下的数据 title 时,没办法触发对应数据的 setter。因为对象 data.course 没有被代理到。
let data = {
stage: "来了来了",
course: {
title: "前端开发",
author: "lucy",
publishTime: "2022年7月23日",
},
};
const observe = (data, isTrue = false) => {
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach((key) => {
let currentVal = data[key];
// 为子集是 json 的也代理递归处理
observe(data[key], true);
if (!isTrue) {
console.log("key:", key, "value:", currentVal);
}
Object.defineProperty(data, key, {
enumerable: true, // 可枚举的
configurable: false, // 可配置的
get() {
console.log(
`getting ${key} value now, value is ${JSON.stringify(currentVal)}`
);
return currentVal;
},
set(newValue) {
currentVal = newValue;
console.log(`setting ${key} value now, setting value is ${newValue}`);
},
});
});
};
observe(data, true);
data.course.title = "修改title使set触发";
上述修改 title 时只是简单的修改成了简单的数据类型,而不是复杂的数据类型。如果 title 被赋值了复杂的数据类型,仍然需要实现一次新数据的代理劫持。
data.course.title = { name: "修改title使set触发" };
// 如果是这种数据类型的赋值,仍然需要进行一次数据代理的劫持
# 监听数组的变化
将上述 data 数据中补充数组形式的数据
let data = {
stage: "来了来了",
course: {
title: "前端开发",
author: "lucy",
publishTime: "2022年7月23日",
},
author: ["lucy", "bobby"],
};
// 监听不到
data.author.push("jojo");
之所监听不到数组的 push 行为,是因为 Object.defineProperty 只是做了数据的代理,而数组的 push 方法是一种行为,Array.property 上挂在的方法并不能触发 data.author 属性值的 setter,因为这并不属于赋值操作。
所以,我们需要重写数组方法,覆盖掉原生的数组方法。
const arrExtend = Object.create(Array.prototype);
const arrMethods = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
arrMethods.forEach((method) => {
const oldMethod = Array.prototype[method];
const newMethod = function (...args) {
oldMethod.apply(this, args);
console.log(`${method}方法被调用了`);
};
arrExtend[method] = newMethod;
});
Array.prototype = Object.assign(Array.prototype, arrExtend);
let array = [1, 2, 3, 4];
array.push(5); // push 方法被调用了
# 代理的升级 Proxy
Proxy 实现数据代理和使用 Object.defineProperty 实现数据代理拦截的对比如下:
- Object.defineProperty 不能监听数组的变化,需要对数组方法进行重写。
- Object.defineProperty 必须遍历对象的每个属性,且需要对嵌套结构进行深层次遍历。
- Proxy 的代理是针对整个对象的,而不是针对对象的某个属性的。所以,不用像 Object.defineProperty 那样必须遍历对象的所有属性。Proxy 只需要做一层代理就可以监听同级结构下所有属性的变化,针对深层结构,递归处理也是需要进行的。
- Proxy 支持代理数组的变化
- Proxy 的第二个参数除了可以使用 set 和 get,还可以使用 13 种拦截方法,比 Object.defineProperty 更为强大
- 使用 Proxy 时,性能将会被底层持续优化;而使用 Object.defineProperty 时,性能已经不再是优化的重点了。