这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战
1. 什么是响应式?
数据变化,视图(也就是DOM)会自动变化。
我们在使用vue差值表达式时,改变数据后,页面上相应数据会自动改变。
2. 实现一个响应式系统,需要做些什么?
我们要实现的是:数据A改变时,视图中用到数据A的所有地方,都要发生改变。
现在将上面的步骤拆为3步:
监听数据A的变化 ---数据劫持(当数据变化时,我们可以做一些特定的事情)收集所有依赖于数据A的元素 ---依赖收集(我们要知道那些视图层的内容(DOM)依赖了哪些数据(state))通知上述这些依赖于数据A的元素更新---派发更新(数据变化后,如何通知依赖这些数据的DOM)
vue实现响应式主要做了这么几件事:
- 数据劫持:new Vue的时候遍历data对象,用Object.defineProperty给所有属性加上了getter和setter
- 依赖的收集:render的过程(执行render()时),会触发数据的getter,在getter的时候把当前的watcher对象收集起来
- 派发更新:setter的时候,遍历这个数据的依赖对象(watcher对象),进行更新
3.数据劫持的实现方式
方式一:Object.defineProperty()
方式二:ES6中新增内置对象:Proxy
Vue2的响应式数据劫持是利用Object.defineProperty()实现的,vue3改成了proxy。
注意:vue3中,基本数据类型的响应式仍是依靠Object.defineProperty()实现的,而对象类型数据使用proxy来实现。
4.为什么vue3使用了 Proxy 替换了原先的 Object.defineproperty 来实现数据响应
Object.defineproperty()的缺点:
- 1.无法检测到对象属性的
新增或`删除(vue2提供了vue.set方法来解决) - 2.当对某个数组arr[index] = val这样赋值时,无法监听数组变化,
- 3.当改变数组长度时,无法监听数组变化。
- 4.深度监听,层层处理,影响性能
- 5.Object.defineproperty()每调用一次都只能对对象的某一个属性进行数据劫持,所以要采用循环遍历,代码写起来比较麻烦。
5. Proxy实现响应式
Proxy内置对象
作用: Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法:
const p = new Proxy(target, handler)
参数:
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
重要方法:
get()
作用: 属性读取操作的捕捉器。
语法:
var p = new Proxy(target, {
get(target, property, receiver) {
}
});
参数:
target 目标对象。
property被获取的属性名。
receiverProxy或者继承Proxy的对象
返回值: get方法可以返回任何值。
set()
作用: 属性设置操作的捕捉器。新增属性操作的捕捉器。
语法:
const p = new Proxy(target, {
set(target, property, value, receiver) {
}
});
参数:
target目标对象。
property将被设置的属性名或 Symbol。
value新属性值。
receiver
最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被 间接地调用(因此不一定是 proxy 本身)。
比如: 假设有一段代码执行 obj.name = "jen", obj 不是一个 proxy,且自身不含 name 属 性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set() 处理器会被调用,而此时,obj 会 作为 receiver 参数传进来。
返回值:
set() 方法应当返回一个布尔值
deleteProperty()
作用: delete 操作符的捕捉器。
语法:
var p = new Proxy(target, {
deleteProperty(target, property) {
}
});
参数:
target目标对象。
property待删除的属性名。
返回值:
deleteProperty 必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。
简单实现响应式
<script type="text/javascript">
// 源数据
let person = {
name: '张三',
age: 18
}
//模拟vue3中实现响应式
cosnt p = new Proxy( person, {
get( target, proName ) {
console.log(`有人读取了p身上的${propName}属性`)
return target[proName]
},
// set在新增属性时,也会调用set
set( target, proName, value ) {
console.log(`有人修改了p身上的${propName}属性,更新页面!`)
target[proName] = value
},
deleteProperty( target, proName ) {
console.log(`有人删除了p身上的${propName}属性,更新页面!`)
return delete target[proName] //返回一个标识删除是否成功的布尔值
}
} )
</script>
加入Reflect
为什么要加入Reflect?
当使用Object.defineProperty追加属性时,不可以重复追加同一个属性。
let obj = {a:1,b:2}
Object.defineProperty(obj, 'c',{
get() {
return 3
}
})
Object.defineProperty(obj, 'c',{
get() {
return 4
}
})
上面的代码会报错,由于JS单线程,会导致整个程序中断,不再运行。
如果不想让程序中断,可以使用try/catch将代码包裹起来,但要多写一些代码,比较麻烦。
而通过Reflect,出现错误时不会报错,也不会导致程序中断;会返回一个false值告诉你操作失败了。
Reflect健壮性更好
<script type="text/javascript"> // 源数据 let person = { name: '张三', age: 18 } //模拟vue3中实现响应式 cosnt p = new Proxy( person, { get( target, proName ) { console.log(`有人读取了p身上的${propName}属性`) return Reflect.get(target, proName) }, // set在新增属性时,也会调用set set( target, proName, value ) { console.log(`有人修改了p身上的${propName}属性,更新页面!`) return Reflect.set( target, proName, value ) }, deleteProperty( target, proName ) { console.log(`有人删除了p身上的${propName}属性,更新页面!`) return Reflect.deleteProperty(target, proName) //返回一个标识删除是否成功的布尔值 } } ) </script>
<script type="text/javascript">
// 源数据
let person = {
name: '张三',
age: 18
}
//模拟vue3中实现响应式
cosnt p = new Proxy( person, {
get( target, proName ) {
console.log(`有人读取了p身上的${propName}属性`)
return Reflect.get(target, proName)
},
// set在新增属性时,也会调用set
set( target, proName, value ) {
console.log(`有人修改了p身上的${propName}属性,更新页面!`)
return Reflect.set( target, proName, value )
},
deleteProperty( target, proName ) {
console.log(`有人删除了p身上的${propName}属性,更新页面!`)
return Reflect.deleteProperty(target, proName) //返回一个标识删除是否成功的布尔值
}
} )
</script>