Vue2响应式原理
实现Vue2响应式原理主要就是通过数据劫持,依赖收集,派发更新的方式来实现的。
-
数据劫持:使用 Object.defineProperty 方法添加对象,重写了原有的 get 和 set 方法;
-
依赖收集:在渲染视图时 将watcher和具体的属性,通过发布订阅者模式管理,这样数据改变之后就能更精准的更新视图,也就是需要用到数据的地方,称为依赖;
-
派发更新:通过dep来执行watcher的notify方法。
在 getter() 中收集依赖,在 setter() 中触发依赖 // 对象类型
在 getter() 中收集依赖,在 拦截器 中触发依赖 // 数组类型
使用Object.defineProperty做响应式的缺点:
- 深度监听,需要一次性递归到底,计算量比较大,如果对象中属性过多,那么需要给每个属性都绑定defineProperty,十分损耗效率。
- 只能监听对象属性的修改、读取,无法监听到对象属性的动态添加和删除;
- 无法原生监听数组,通过下标、length修改数组也不会响应式刷新页面,可以直接重新赋值,或者使用filter、map、concat、slice等生成新数组对其赋值。
解决方案:使用this.$set 、this.$delete
this.$set(this.arr, index, value)
Vue.set(this.arr, index, value)
this.$delete(this.arr, index)
Vue.delete(this.arr, index)
// 操作数组的函数
splice(),push(), pop(), shift(), unshift(), sort(), reverse()
相应式实例代码:
let person = {
name:'anna',
age:18
}
let p = {}
Object.defineProperty(p, "name", {
// 获取 name 时调用
get(){
return person.name;
},
// 设置 name 时调用
set(value){
console.log("修改 name 属性")
person.name = value
}
});
Object.defineProperty(p, "age", {
// 获取 age 时调用
get(){
return person.age;
},
// 设置 age 时调用
set(value){
console.log("修改 age 属性")
person.age = value
}
});
此时,p对象就完成了对person对象的代理,当读取p.name时,实际上是在读取person.name,当修改p.name时,实际上person中name属性的值也会随之更新。
但是,在Vue2中,无法通过p对象对person对象进行增和删的操作,实际上person对象是捕获不到的,所以即便通过p对象删除和增加属性,person对象内的属性是不会更新的。
基于Vue2的缺点,在Vue3 中得到了解决。
Vue3中的响应式原理
和Vue2不同的是,它的核心是es6的 Proxy 结合 Reflect 实现的,使用代理,本质上是通过 `Proxy` 劫持了数据对象的读写,当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知;这样就可以不直接操作对象,也可以监听到所有对象的增删改查,同时也提升了效率。
Proxy:
es6新增了Proxy类,即代理的作用。如果要监听一个对象,可以先创建代理对象,之后对对象的所有操作,都由代理对象完成,它可以监听我们的所有操作。支持的拦截操作共 `13` 种。
// 基本使用
const p = new Proxy( target, handler );
target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
let person = {
name: "anna",
age: 18,
};
let p = new Proxy(person, {
get(target, key) {
return target[key];
},
set(target, key, val) {
return (target[key] = val);
},
});
console.log(p);
//测试 get 是否可以拦截成功
console.log(p.name); // 输出 anna
console.log(p.age); // 输出 18
console.log(p.job); // 输出 undefined
//测试 set 是否可以拦截成功
p.age = 25;
console.log(p.age);
Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。
Reflect:
`Reflect`对象与`Proxy`对象一样,也是 ES6 为了操作对象而提供的新 API,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy 的方法一一对应,也是 `13` 种。Reflect就是为了让this指向代理对象。
let person = {
name: "anna",
age: 18,
};
const objProxy = new Proxy(person, {
get(target, key, receiver) {
console.log("属性被访问");
return Reflect.get(target, key)
},
set(target, key, newValue, receiver) {
console.log("属性被修改");
Reflect.set(target, key, newValue)
}
})
objProxy.name = 'lily'
objProxy.age = 25
console.log(objProxy.name);
console.log(objProxy.age);
基本数据类型通过ref实现响应式,引用数据类型通过reactive实现响应式,可以拦截对象中任意操作的变化,包括属性的读写、属性添加、属性删除,以及数组下标的修改。
setup() {
// 没有响应式
let msg = "hello";
function changeMsg() {
msg = "hello world";
}
// 通过 ref 定义响应式变量:基本数据类型
// ref() 返回带有 value 属性的对象
let counter = ref(0);
function changeCounter() {
counter.value++;
}
// 通过 reactive 定义引用类型的数据:对象和数组,引用类型数据
let obj = reactive({
name: "anna",
age: 18,
person: {
name: "lily",
},
});
function changeObj() {
obj.person.name = "bob";
obj.name = "bob";
}
// toRefs 使解构后的数据重新获得响应式
// 通过 es6 扩展解构运算符进行解构使得对象中的属性不是响应式的
return {
msg,
changeMsg,
counter,
changeCounter,
obj,
changeObj,
...toRefs(obj),
};
},
<template>
<div class="hello">
<h1>{
{ msg }}</h1>
<h1>hello</h1>
<h1>{
{ counter }}</h1>
<h1>{
{ obj.person.name }}</h1>
<h1>{
{ obj.name }}</h1>
<h1>{
{ person.name }}</h1>
<h1>{
{ name }}</h1>
<button @click="changeMsg">改变msg</button>
<button @click="changeCounter">改变counter</button>
<button @click="changeObj">改变obj</button>
</div>
</template>
利用Proxy实现数据劫持的优势:
- 解决了使用 Object.defineProperty() 数据劫持的缺点,完美监听任何方式导致的数据改变行为;
- 对于整个拦截对象直接进行挟持,无需遍历属性依次添加定义get和set属性;
- 对于原对象生成拦截对象,然后对拦截对象进行相应监听行为,确保原对象不变。
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 new Proxy() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由 Object.defineProperty 更改为Proxy代理。
Object.defineProperty() 和 Proxy 的区别
Proxy
优点:
- 可以直接监听对象而非属性;
- 可以直接监听数组的变化;
- 有13种拦截方法(Object.defineProperty() 没有);
- 返回的是一个新对象,我们可以直接操作新对象来达到目的,而Object.defineProperty() 只能遍历对象属性直接修改;
- 性能优化。
缺点:
- 无法兼容所有浏览器,比如IE11,无法进行polyfill。
Object.defineProperty()
优点:
- 兼容性较好
缺点:
- 无法对数组进行监听,采用的是对数组的方法进行重写(push、pop、shift、unshift等),对此进行双向绑定和数据监听的操作;
- 效率差,因为对多层数据进行一次性的递归操作,当数组多时,非常影响性能。
总结:为什么vue3响应式优于vue2响应式
- Vue2的响应式是基于`Object.defineProperty`实现的
- Vue3的响应式是基于ES6的`Proxy+Reflect`来实现的
vue2中存在的问题
- 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
- 对象的新增属性、删除属性、界面不会更新
- vue2的解决方法: 通过vm.$set(obj,key.val)/vm.$delete(obj,key)新增属性/删除属性
- 直接通过下标修改数组,界面不会自动更新
- vue2的解决方法:vm.$set(arr,index.value),调用数组的splice方法
vue3中的优化
vue2是监听对象的属性,vue3是监听对象
- 不需要一次性遍历data的属性,可以显著提高性能。需要的时候再进行深度监听。
- 基于Proxy,对被代理对象设置拦截,访问或操作被代理对象都要先通过拦截。所以可以监听对象属性的添加和删除。
- 可以原生监听数组,不需要通过重写方法来实现对数组的监控。