响应式reactivity 是 Vue 3 相对于 Vue 2 改动比较大的一个模块,也是性能提升最多的一个模块,其核心改变是采用了 ES6的 Proxy API 来代替 Vue 2 中的 Object.defineProperty 方法来实现响应式。
在 Vue 3 中使用响应式对象的方法如下:
import {ref,reactive] from 'vue'
...
setup () {
const name = ref('test')
const state = reactive({
list: []
})
return {name,state]
}
...
在 Vue 3 中,组合式 API 中经常会使用创建响应式对象的方法 ref/reactive,其内部就是利用Proxy API 来实现的,特别是借助 handler 的 set 方法可以实现双向数据绑定相关的逻辑,这对于Vue 2 中的 Object.defineProperty()是很大的改变,主要提升如下:
- Objcct.defineProperty()只能单一地监听已有属性的修改或者变化,无法检测到对象属性的新增或删除 (Vue2 中采用$set()方法来解决),而 Proxy 则可以轻松实现。
- Object.defineProperty()无法监听响应式数据类型是数组的化( 主要是数组长度的变化, Vu2中采用重写数组相关方法并添加钩子来解决),而 Proxy 则可以轻松实现。
正是由于 Proxy 的特性,在原本使用 Object.defineProperty()需要很复杂的方式才能实现的上面两种能力,在 Proxy 无须任何配置,利用其原生的特性就可以轻松实现。
Poxy API 对应的 Proxy 对象是 ES6 就已引入的一个原生对象,用于定义基本操作的自定义行为 (如属性查找、赋值、枚举、函数调用等) 。
从字面意思来理解,Proxy 对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等)都必须通过该代理器。因此,我们可以对来自外界的所有操作都进行拦截、过滤、修改等操作。
基于 Proxy 的这些特性常用于:
- 创建一个“响应式”的对象,例如 Vue3.0中的reactive方法。
- 创建可隔离的 JavaScript“沙箱”。
Proxy 的基本语法如下:
const p = new Proxy(target, handler)
其中,target 参数表示要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组函数,甚至另一个代理); handler 参数表示以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。常见的使用方法如下:
let foo = {
a: 1,
b: 2
}
let handler = {
get:(obj,key)=>{
console.log('get')
return key in obj ? obj[key] : undefined
}
}
let p = new Proxy(foo,handler)
console.log(p.a) // 打印1
上面的代码中,p 就是 foo 的代理对象,对 p 对象的相关操作都会同步到 foo 对象上。同时,Proxy也提供了另一种生成代理对象的方法 Proxy.revocable(),代码如下:
const ( proxy,revoke ) = Proxy.revocable(target, handler)
该方法的返回值是一个对象,其结构为: {“proxy": proxy, "revoke": revoke)。其中,proxy表示新生成的代理对象本身,和一般方式 new Proxy ( target, handler ) 创建的代理对象没什么不同, 只是它可以被撤销掉; revoke表示撤销方法,调用的时候不需要加任何参数就可以撤销掉和它一起生成的那个代理对象。代码如下:
let foo = {
a: 1,
b: 2
}
let handler = {
get:(obj,key) => {
console.log('get')
return key in obj ? obj[key] : undefined
}
}
let { proxy,revoke ) = Proxy.revocable(foo,handler)
console.log(proxy.a) // 打印1
revoke()
console.log(proxy.a) // 报错信息: Uncaught TypeError: Cannot perform 'get'on a proxy that has been revoked
需要注意的是,一旦某个代理对象被撤销,它将变得几乎完全不可调用,在它身上执行任何的可代理操作都会抛出 TypeError 异常。
在上面的代码中,我们只使用了 get 操作的 handler,即当尝试获取对象的某个属性时会进入这个方法。除此之外,Proxy 共有接近 13 个 handler,也可以称为钩子,它们分别是:
- handler.getPrototypeOf(): 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy)时。
- handler.setPrototypeOf(): 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null)时
- handler.isExtensible(): 在判断一个代理对象是否可扩展时触发该操作,比如在执行Object.isExtensible(proxy)时。
- handler.preventExtensions(): 在让一个代理对象不可扩展时触发该操作,比如在执行Object.preventExtensions(proxy)时。
- handler.getOwnPropertyDescriptor0: 在获取代理对象某个属性的描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo")时。
- handler.defineProperty(): 在定义代理对象某个属性的描述时触发该操作,比如在执行Object.defineProperty(proxy, "foo",)时。
- handler.has(): 在判断代理对象是否拥有某个属性时触发该操作,比如在执行“foo"in proxy时。
- handlerget(): 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。
- handler.set(): 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo=1时。
- handlcr.deleteProperty(): 在除代理对象的某个属性时触发该操作,即使用 delete 运算符比如在执行 delete proxy.foo 时。
- handlcr.ownkeys():当执行 Object.get0wnPropertyNames(proxy)和Object.getOwnPropertySymbols(proxy)时触发。
- handler.apply(): 当代理对象是一个 fnction 函数,调用 apply0方法时触发,比如 proxy.apply。
- handlcr.construct(): 当代理对象是一个 function函数,通过 new 关键字实例化时触发,比new proxy()。
结合这些 handler,我们可以实现一些针对对象的限制操作,例如:
禁止删除和修改对象的某个属性,代码如下:
let foo = f
a:l,
b:2
}
let handler = {
set: (obj,key,value,receiver) => {
console.log('set')
if (key == 'a') throw new Error('can not change property:'+key)
obj[key] = value
return true
},
deleteProperty: (obj,key) => {
console.log('delete')
if (key == 'a') throw new Error('can not delete property:'+key)
delete obj[key]
return true
}
}
let p = new Proxy(foo,handler)
// 尝试修改属性 a
p.a = 3 // 报错信息: Uncaught Error
// 尝试删除属性 a
delete p.a // 报错信息: Uncaught Error
上面的代码中,set 方法多了一个 receiver 参数,这个参数通常是 Proxy 本身(即 p),场景是当有一段代码执行 obj.name="jen”时,obj不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么那个 proxy 的 handler 中的 set 方法会被调用,而此时 obj 会作为 receiver参数传进来。
对属性的修改进行校验,代码如下:
let foo = {
a:1,
b:2
}
let handler = {
set:(obj,key,value) => {
obj[key] = value
console.log('set')
if (typeof(value) !== 'number') throw new Error('can not change property:'+key)
return true
}
}
let p = new Proxy(foo,handler)
p.a = 'hello' // 报错信息: Uncaught Error
Proxy 也能监听到数组变化,代码如下:
let arr = [1]
let handler = {
set:(obj,key,value)=>(
console.log('set') // 打印 set
return Reflect.set(obj, key, value);
}
}
let p = new Proxy(arr,handler)
p.push(2) // 改变数组
Reflect.set()用于修改数组的值,返回布尔类型,也可以用在修改数组原型链上的方法的场景.
相当于 obj[key] = value。
ref()方法运行原理
待续...