Vue的响应式原理
vue2.x的响应式原理
实现原理:
- 对象类型:通过Object.defineProperty()对属性进行读取、修改及拦截,这种方式叫做数据劫持
- 数组类型:通过重写更新数组的方法来实现拦截。举个例子:push方法,vue里面的push方法是经过二次封装的,分为两个步骤。第一步是调用数组的push方法,第二步是更新页面。
// 这里举一个例子
// 这里有一个peson对象里面有name和age
// 按钮的点击事件是为了添加一个sex属性给person对象
// 这里的add是vue里面method的函数
add(){
this.person.sex = "女"; // 这样操作,是不会成功的。对象已经被修改了,但是没有被更新到页面上
// 在vue2.x里面为了解决这个问题,开发者为用户提供了一个$set属性,通过$set添加的属性,可以被更新到页面上
this.$set(this.person,"sex","女"); // 这样操作当我们触发点击事件之后,person添加的信息可以成功的被添加到页面上
// 当然我们也可以通过Vue的方法来改变,如果要通过这种方式修改的话,就要引入Vue
Vue.set(this.person,"sex","女");
}
// 在添加一个点击事件,点击之后删除person的name属性
delete(){
// 在js里面我们想要删除一个属性就是delete 对象实例的属性
delete this.person.name; // 但是这样删除,页面是检测不到的
// Object.defineProperty()里面只写两个东西,一个是get用来获取数据,一个是set用来修改数据,新增和删除都是检测不到的
// 那么有$set,就会有$delete
this.$delete(this.person,"name"); // 这样就能成功的删除对象的那么属性了
// 同样和上面的添加一样,也可以使用vue的方法来删除数据
Vue.delete(this.person,"name");
}
// 这里对对象的响应式的一些问题进行了解决
// 下面来解决数组的问题
// 给对象添加一个新的属性--爱好,爱好是一个数组。['学习','吃饭']
// 添加一个点击事件,当点击之后将数组的第一个元素改为逛街
update(){
// 在js里面修改数组通常就是下面这种形式
this.person.hobby[0] = "逛街"; // 这种方式修改不会成功的
// 和上面一样,要实现响应式我们可以通过$set的形式来修改数组的数据
this.$set(this.person.hobby,0,"逛街");
// Vue的方法同样也可以,这里还有另一种方法就是splice 替换数组中的某个元素
this.person.hobby.splice(0,1,"逛街") //这样操作也是可以实现响应式的
}
vue2的响应式存在一些问题:两种方式都修改了数据,但是数据并没有更新到页面。
- 新增属性、删除属性,界面不会更新
- 直接通过数组下标修改数组,界面不会自动更新
虽然vue2有响应式的问题,但是至少提供了解决的办法。
vue3的响应式原理
vue3解决了vue2.x中出现的响应式问题。直接对对象操作就可以增加和删除元素,数组也是一样的,前提是对象或者数组要用reactive绑定。
实现原理:
- 通过Proxy代理:拦截对象中任意属性的变化,包括属性的读写、删除、添加等
- 通过Reflect反射:对源对象的属性进行操作。
// 首先要创建一个源对象person,然后利用Proxy代理person
// 模拟vue3实现响应式
// Proxy第一个参数是源对象,第二个数配置对象可以为空
const p = new Proxy(person,{}); // 这样可以做到修改源对象的同时修改目标对象,但是这样不叫作响应式,响应式是修改了要检测到修改了这个过程
// 这里不写get,set,deleteProperty也能实现对象的操作,为什么要写呢?
// 就是为了捕获这个过程,实现响应式的这样一个过程。
// 修改之后的p
const p = new Proxy(person,{
// 但是这样检测不到我们具体获取了哪一个属性
// 这时,代理的get和set就会有两个参数,一个是target源对象,第二个是属性名
get(target,propname){
console.log("检测到p的某个属性被读取了",target,propname);
// 如果我们想要返回源对象的属性值有人可能就会像下面这么写
return target.propname; // 乍一看没什么问题,但是要知道,这里的propname是一个形参,target对象里面是没有一个名为propname这个属性的
// 这时,要返回一个target的属性值的话,就要通过下面这种形式返回
return target[propname]; // 这样才能返回正确的数据,不然会返回undefined,表示你的对象里面没有定义,也印证了上面的说法
},
// set的参数会多一个,就是修改的值
set(target,propname,value){
console.log("检测到p的某个属性被修改了",target,propname,value);
target[propname] = value;
}
// 上面只有获取和修改,那添加和删除怎么办呢?
// 删除的话,Proxy里面给出了一个配置,参数的意思和上面一样
deleteProperty(target,propname){
console.log("检测到p的某个属性被删除了",target,propname);
return delete target[propname];
}
// 删除都有了,那添加一个属性怎么办呢?
// 这里添加一个属性是直接通过set来添加一个属性,当你添加一个属性的时候,set里面的东西会被输出。
});
这里并没有完全的实现响应式,vue3实现响应式的原理,里面还用了一个技术Reflect(反射)
ECMA组织正尝试着把Object上面的一些有用的API移植到Reflect上面,举个例子Object.defineProperty(),在Reflect上面也有这个方法。
当我们使用Object.defineProperty()的时候,如果遇到重名的话,控制台会抛出错误,然后直接就挂了,后面的代码都不会执行。
如果使用Reflect.defineProperty()的话,和上面同样的错误,控制台不会抛出错误,而且代码不会挂掉。
但是不抛出错误也不行呀,所以Reflect.defineProperty()或返回一个Boolean值。通过判断Boolean值,来判断是否出现错误。如果这样搞,那么有人会觉得不如用Object的方法。对于操作对象来说,确实Object更好,能够及时的反馈出来错误。但是对于一个框架来说,让代码挂掉就是一个非常非常致命的东西,虽然我们有TryCatch的操作,但是就变得很麻烦了这样会写很多个trycatch,这时候Reflect就体现出它的作用了,可以通过Refelct的返回值,判断是否错误。
所以上面的代码修改为下面这样的就是vue3实现响应式的一个雏形了:
const p = new Proxy(person,{
get(target,propname){
console.log("检测到p的某个属性被读取了",target,propname);
return Reflect.get(target[propname]);
},
set(target,propname,value){
console.log("检测到p的某个属性被修改了",target,propname,value);
Reflect.set(target,propname,value);
}
deleteProperty(target,propname){
console.log("检测到p的某个属性被删除了",target,propname);
return Reflect.deleteProperty(target,propname);
}
});