数据劫持的目的
VUE2.0和VUE3.0实现响应式的底层逻辑,是对数据做劫持,为什么要劫持数据呢?是因为,劫持数据后才可以,在更改数据同时对页面进行重新渲染,从而达到响应式。
VUE3.0响应原理
VUE3.0使用了ES6 proxy做代理,proxy是用于创建一个对象的代理,可实现基本操作的拦截和自定义。简而言之,proxy可以在数据做操作时,进行拦截,其基础语法是:const p = new Proxy(target,handler)
target为要操作的obj,handler是数据的set,get操作方法。
举个例子:
let test = reactive({ a: 1, b: 2 });
function reactive(target) {
const testProxy = new Proxy(target, {
get(target, key, receiver) {
//拦截后可以做很多事情,比如视图渲染
const res = Reflect.get(target, key, receiver); //等同于res=target[key]
console.log("响应式获取:" + res);
return res;
},
set(target, key, value, receiver) {
//拦截后可以做很多事情,比如视图渲染
const res = Reflect.set(target, key, value, receiver); //等同于设置target[key]=value
console.log("响应式设置:" + res);
}
});
return testProxy;
}
test.a;
test.a = 3;
运行结果:
以上例子是reactive核心思想,其本质也就是对对象进行了代理,并返回代理对象。在get、set方法中使用了Reflect对象,**注意:这是个对象,并不是方法。**官方解释,Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。那为什么在这里使用Reflect对象呢?因为,Reflect与Proxy的get(),set()方法一一对应,一个Proxy的get方法对应一个Reflect.get()方法,如果使用obj.defineProperty方法修改两次同一属性的,就会报错。JS又是单线程的,报错后不会继续执行下面代码,则框架的健壮性就会很差。
let test = { a: 1, b: 2 };
Object.defineProperty(test, "c", {
get() {
return 7;
}
});
Object.defineProperty(test, "c", {
get() {
return 99;
}
});
但如果使用Reflect,就不会报错,会将数据反射出去。
let test = { a: 1, b: 2 };
const t1 = new Proxy(test, {
get() {
return 7;
}
});
console.log("t1.c=" + t1.c);
const t2 = new Proxy(test, {
get() {
return 99;
}
});
console.log("t2.c=" + t2.c);
VUE2.0响应式原理
vue2.0响应式是通过Object.defineProperty方法,对属性进行劫持。官方文档解释,Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。基础语法是:
Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
一个字符串或 Symbol,指定了要定义或修改的属性键。
descriptor
要定义或修改的属性的描述符。
可以看出,Object.defineProperty是需要对对象里的每个属性,一一进行设置或者监听。在VUE2中,也是使用该核心思想,但此方法只可以对对象进行监听,数组Array监听则采用对数组原型方法重写,进行数据拦截。
- 对象Object监听
let test = { a: 1, b: 2 };
observer(test);
function observer(obj) {
let testKeys = Object.keys(test);
testKeys.map((key) => {
defineProperty(obj, key, obj[key]);
});
}
function defineProperty(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log("响应式获取:" + val);
return val;
},
set(newval) {
console.log("响应式设置:" + key + "=" + newval);
val = newval;
}
});
}
console.log(test.a);
//响应式获取:1
// 1
test.a = 5;
//响应式设置:a=5
console.log(test.a);
//响应式获取:5
// 5
源码核心思想如上,会建立一个observer监听对象,并在defineProperty中实现对每个属性的数据拦截。
如果是多层对象嵌套,可通过递归方式,对属性中数据做深入拦截。
let test = { a: 1, b: { c: 2, d: 3 } };
observer(test);
function observer(obj) {
if (typeof obj !== "object") return;
let testKeys = Object.keys(obj);
testKeys.map((key) => {
defineProperty(obj, key, obj[key]);
});
}
function defineProperty(obj, key, val) {
if (typeof val === "object" || val !== null) {
observer(val);
}
Object.defineProperty(obj, key, {
get() {
console.log("响应式获取:" + val);
return val;
},
set(newval) {
console.log("响应式设置:" + key + "=" + newval);
val = newval;
}
});
}
- 数组Array监听
数组的数据劫持,主要是对数组中,会影响数组结构和内容的方法,在原型链上重写。这些方法包括push、pop、shifit等,这是因为数组结构发生变化,需要视图进行同步更新,所以需要对这些方法进行劫持。
const customMethods = [
"push",
"pop",
"shifit",
"unshift",
"splice",
"sort",
"reverse"
];
const originArrMethods = Array.prototype, //获取Array上的原型对象,包含所有方法
arrMethods = Object.create(originArrMethods); //创建一个和原型方法一样的对象
customMethods.map((m) => {
//重写方法
arrMethods[m] = function () {
const args = Array.prototype.slice.call(arguments), //将入参处理为数组
rt = originArrMethods[m].apply(this, args); //将this指向改为原始Array
let newArr;
switch (m) {
case "push":
case "unshift":
newArr = args;
break;
case "splice": //splice(start,end,新值)
newArr = args.slice(2); //取新值
break;
default:
break;
}
return rt;
};
});