Vue2.x 的数据劫持
Object.defineProperty()
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
const object1 = {};
Object.defineProperty(object1, "property1", {
value: 42,
writable: false,
});
object1.property1 = 77; // throws an error in strict mode
console.log(object1.property1); // 42
语法
Object.defineProperty(obj, prop, descriptor);
参数
- obj - 要定义属性的对象。
- prop - 要定义或修改的属性的名称或 Symbol 。
- descriptor - 要定义或修改的属性描述符。
描述符介绍
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):
- configurable - 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
- enumerable - 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false。
数据描述符还具有以下可选键值:
- value - 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
- writable - 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。默认为 false。
数据描述符还具有以下可选键值:
- get - 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
- set - 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。
注意:
-
如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。
-
如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。
-
这些选项不一定是自身属性,也要考虑继承来的属性。为了确认保留这些默认值,在设置之前,可能要冻结 Object.prototype,明确指定所有的选项,或者通过 Object.create(null) 将 proto (en-US) 属性指向 null。
返回值
被传递给函数的对象。
注意: 在 ES6 中,由于 Symbol 类型的特殊性,用 Symbol 类型的值来做对象的 key 与常规的定义或修改不同,而 Object.defineProperty 是定义 key 为 Symbol 的属性的方法之一。
Vue2.x 的数据劫持
/**
* 响应式 - 数据变了会通知更新视图
*/
function updateView() {
console.log("更新视图");
}
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
// 重写部分能直接修改原数组的方法
["push", "shift", "pop", "unshift", "splice"].forEach((method) => {
proto[method] = function () {
// 改写方法,但是还是要调用老的方法实现功能
updateView();
oldArrayPrototype[method].call(this, ...arguments);
};
});
/**
* 声明一个观察者
*/
function observer(target) {
if (typeof target !== "object" || target == null) {
return target;
}
if (Array.isArray(target)) {
// 拦截数组,给数组的原型方法
target.__proto__ = proto;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
defineReactive(target, key, target[key]);
}
}
}
/**
* 定义一个响应式方法
*/
function defineReactive(target, key, value) {
observer(value);
Object.defineProperty(target, key, {
get() {
console.log("get value===", value);
return value;
},
set(newValue) {
if (newValue !== value) {
updateView();
console.log("set value===", newValue);
value = newValue;
}
},
});
}
测试(以下贴出了所以的控制台打印信息,{}都是可以展开的,展开也会调用 get 方法)
let data = {
name: "frank",
age: 20,
otherInfo: {
sex: "male",
},
clothes: ["shirt", "pants", "shoes"],
frends: [
{ name: "link", age: 22 },
{ name: "lottery", age: 19 },
],
};
// 开始观察
observer(data);
data.age += 1;
// get value=== 20
// 更新视图
// set value=== 21
data.otherInfo.sex = "m";
// get value=== {}
// 更新视图
// set value=== m
data.clothes.unshift("hat");
// get value=== (3) [(...), (...), (...)]
// get value=== shirt
// get value=== pants
// get value=== shoes
// 更新视图
// get value=== shoes
// get value=== pants
// 更新视图
// set value=== pants
// get value=== shirt
// 更新视图
// set value=== shirt
// 更新视图
// set value=== hat
data.clothes.shift();
// get value=== (4) [(...), (...), (...), "shoes"]
// get value=== hat
// get value=== shirt
// get value=== pants
// 更新视图
// get value=== hat
// get value=== shirt
// 更新视图
// set value=== shirt
// get value=== pants
// 更新视图
// set value=== pants
// 更新视图
// set value=== shoes
data.frends.push("json");
// get value=== (2) [(...), (...)]
// get value=== {}
// get value=== {}
// 更新视图
data.frends.pop();
// get value=== (3) [(...), (...), "json"]
// get value=== {}
// get value=== {}
// 更新视图