vue3响应式原理--Proxy

1,230 阅读3分钟

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

1. 什么是响应式?

数据变化,视图(也就是DOM)会自动变化。

我们在使用vue差值表达式时,改变数据后,页面上相应数据会自动改变。

2. 实现一个响应式系统,需要做些什么?

我们要实现的是:数据A改变时,视图中用到数据A的所有地方,都要发生改变。

现在将上面的步骤拆为3步:

  • 监听数据A的变化 --- 数据劫持 (当数据变化时,我们可以做一些特定的事情)
  • 收集所有依赖于数据A的元素 --- 依赖收集 (我们要知道那些视图层的内容(DOM)依赖了哪些数据(state))
  • 通知上述这些依赖于数据A的元素更新 --- 派发更新 (数据变化后,如何通知依赖这些数据的DOM)

vue实现响应式主要做了这么几件事:

  1. 数据劫持:new Vue的时候遍历data对象,用Object.defineProperty给所有属性加上了getter和setter
  2. 依赖的收集:render的过程(执行render()时),会触发数据的getter,在getter的时候把当前的watcher对象收集起来
  3. 派发更新: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>