Vue3响应式方案以及ref reactive的区别

本文深入探讨Vue3的响应式方案,包括ref和reactive的区别。Vue2中响应式依赖于Object.defineProperty,而Vue3采用Proxy和Reflect实现更强大的数据代理。文章详细解释了Proxy的拦截机制、Reflect的作用,以及reactive和createReactiveObject、mutableHandlers、mutableInstrumentations的关系。同时,介绍了ref的实现原理,包括createRef、toReactive和proxyRefs的功能。通过源码分析,揭示了ref在模板中如何自动脱ref的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

距离 Vue3 出来了已经有一段时间了, 最近呢重温了一下Vue3的响应式方案,以及ref reactive的区别,相信你看完本文,也能对 Vue3 响应式的了解有所提高

在看源码的过程中,时常会感到框架的设计之美,看起来也会感到赏心悦目, 这也是能坚持把源码看下去的动力

二、新的方案

1. 缘由

  • 已知在 Vue2 中, 响应式原理一直是采用 Object.defineProperty 来进行,那这样做有什么权限呢? 下面一一道来* 这个API, 只能拦截 get / set 的属性* 如对象 新增 或者 删除 了属性,则无法监听到改变* 对于数组,若使用数组的原生方法改变数组元素的时候 也无法监听到改变
  • 所以呢在Vue3中采用了 ProxyReflect搭配来代理数据2. Proxy 和 Reflect

1) Proxy

既然Vue3中响应式数据是基于 Proxy 实现的,那么什么是Proxy呢?

使用Proxy可以创建一个代理对象,它可以实现对 对象数据代理, 所以它 无法对非对象值进行代理,也就是为什么Vue3中对于非对象值要使用 ref 来进行响应式的原因 (后面讲解ref的时候再细说)

  • 代理是指 允许我们拦截并重新定义对一个对象的基本操作。 例如: 拦截读取、 修改等操作.
const obj = {}

const newP = new Proxy(obj, { // 拦截读取get(){/*...*/ },// 拦截设置属性操作set(){/*...*/ }
}) 

2) Reflect

说完了Proxy, 接下来我们来说说 Reflect

通过观察 MDN 官网可以发现, Reflect的方法Proxy的拦截器方法 名字基本一致

那就出现了一个问题,我们为什么要用 Reflect 呢

主要还是它的第三个参数,你可以理解为函数调用过程中的this,我们来看看它配合 Proxy 具体的用途吧

const obj = {foo: 1,// obj 中有一个 getter属性 通过this获取foo的值get getFoo() { return this.foo; }
};

const newP = new Proxy(obj,{// 拦截读取get(target, key) {console.log('读取', key); // 注意这里目前没有使用 Reflectreturn target[key];},// 拦截设置属性操作set(target, key, newVal) {console.log('修改', key);target[key] = newVal}})

obj.foo++
console.log(newP.getFoo); 

执行上面代码你会发现, 在 Proxy 中 get 拦截的中,只会触发对 getFoo 属性进行读取的拦截, 而无法触发在 getFoo 里面对 this.foo 进行读取的拦截!

问题就出现在 getFoo 这个getter里, 这里面的 this 在我们 未使用 Reflect 的时候指向它的原始对象,所以我们才无法通过 Proxy 拦截到属性读取

只需修改一下上面代码中 Proxy 里面的 get 拦截方法

 // 拦截读取get(target, key, receiver) {console.log('读取', key);return Reflect.get(target, key, receiver); // 使用 Reflect返回读取的属性值}, 

这下再执行上面的例子,就会发现能正常对 getFoo 里面的 foo 属性进行读取的拦截。 因为这个时候的 this 已经指向了代理对象 newP

以上呢,就是对 Proxy 和 Reflect 的简易讲解,接下来我们讲讲 Vue3 中的 reactive

3. reactive

看源码会发现,我们平时使用 reactive 的时候,会调用一个 createReactiveObject 的方法

这个地方在: packages\reactivity\src\reactive

 export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {// if trying to observe a readonly proxy, return the readonly version.if (isReadonly(target)) {return target}return createReactiveObject(target,false,mutableHandlers,// 普通对象的 handlersmutableCollectionHandlers, // Set Map 等类型的 handlersreactiveMap)
} 

1) createReactiveObject() 函数

其中主要是做一些前置判断,然后建立响应式地图

WeakMap -> Map -> Set

function createReactiveObject( target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<Target, any> ) {//若目标数据是不是对象则直接返回if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// target is already a Proxy, return it.// exception: calling readonly() on a reactive object// raw 代表原始数据// 或者是非响应式数据就直接返回 原数据if (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// 如已被代理则直接返回代理的这个对象const existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// only a whitelist of value types can be observed.// 只有在白名单中的类型才可以被代理const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}// 建立代理 Proxyconst proxy = new Proxy(target,// 使用不同的 hanlderstargetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)// 存储到响应式地图中proxyMap.set(target, proxy)return proxy
} 

2) mutableHandlers() 函数 -> 对象类型的 handlers

这个地方在: packages\reactivity\src\baseHandlers

主要讲讲getset

export const mutableHandlers: ProxyHandler<object> = {get: createGetter(), // 读取属性set: createSetter(), // 设置属性deleteProperty,// 删除属性has, // 判断是否存在对应属性ownKeys// 获取自身的属性值
} 

get

function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {// 判断返回一些特定的值 例如 是 readonly 的就返回 readonlyMap,是 reactive 的就返回 reactiveMap 等等if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow} else if (key === ReactiveFlags.RAW &&receiver ===(isReadonly? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: reactiveMap).get(target)) {return target}// 如果是数组要进行一些特殊处理const targetIsArray = isArray(target)if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {//重写数组的方法// 'includes', 'indexOf', 'lastIndexOf', 'push', 'pop', 'shift', 'unshift', 'splice'return Reflect.get(arrayInstrumentations, key, receiver)}// 获取属性值const res = Reflect.get(target, key, receiver)if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}// 如果非只读属性 才进行依赖收集if (!isReadonly) {track(target, TrackOpTypes.GET, key)}// 浅层响应则直接返回对应的值if (shallow) {return res}// 如果是ref 则自动进行 脱refif (isRef(res)) {// ref unwrapping - does not apply for Array + integer key.const shouldUnwrap = !targetIsArray || !isIntegerKey(key)return shouldUnwrap ? res.value : res}// 返回值是对象// 如果是只读就用 readonly 包裹返回数据// 否则则进行递归深层包裹 reactive 返回 Proxy 代理对象if (isObject(res)) {// Convert returned value into a proxy as well. we do the isObject check// here to avoid invalid value warning. Also need to lazy access readonly// and reactive here to avoid circular dependency.return isReadonly ? readonly(res) : reactive(res)}// 如都不是上面的判断 则返回这个数据return res}
} 

set

function createSetter(shallow = false) {return function set( target: object,key: string | symbol,value: unknown,receiver: object ): boolean {// 缓存旧值let oldValue = (target as any)[key]if (!shallow && !isReadonly(value)) {if (!isShallow(value)) {value = toRaw(value)oldValue = toRaw(oldValue)}// 若是 ref 并且非只读 则直接修改 ref的值if (!isArray(target) && isRef(oldValue) && !isRef(value)) {oldValue.value = valuereturn true}} else {// in shallow mode, objects are set as-is regardless of reactive or not}// 是否有对于的keyconst hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key)// 修改对应的值const result = Reflect.set(target, key, value, receiver)// don't trigger if target is something up in the prototype chain of original// 若目标是原型链上的内容就不触发依赖if (target === toRaw(receiver)) {// 这里主要是判断是 新增属性 还是修改属性的操作if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)}}// 最终返回结果return result}
} 

3) mutableInstrumentations() 函数 -> Map Set等类型的 handlers

这个地方在: packages\reactivity\src\collectionHandlers

其主要是为了解决 代理对象 无法访问集合类型的属性和方法

function createInstrumentations() { // 主要就是代理了 Map Set等类型的方法 具体实现各位可以去上面地址中的文件里查看const mutableInstrumentations: Record<string, Function> = {get(this: MapTypes, key: unknown) {return get(this, key)},get size() {return size(this as unknown as IterableCollections)},has,add,set,delete: deleteEntry,clear,forEach: createForEach(false, false)}const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]iteratorMethods.forEach(method => {mutableInstrumentations[method as string] = createIterableMethod(method,false,false)})return [mutableInstrumentations]
} 

4. ref

之前说过 Proxy 代理的必须是对象数据类型,而非对象数据类型 例如: string number 等等 则不能用其进行代理, 所以有了 ref 的概念

联想到上面说的 reactive 和我们日常使用的 .value 的形式, 是不是就认为 ref 直接把原始数据包裹成对象 然后通过 Proxy 进行代理的呢?

最开始我也以为是这样,但是查看了源码中发现其实并不是, 其实是创建 ref 的时候, 实例化了一个 class -> new RefImpl(rawValue, shallow) ,然后通过自定义的 get set来进行依赖收集和依赖更新

源码地址: packages\reactivity\src\ref

1) createRef()

export function ref(value?: unknown) { // 调用创建方法return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {// 如果已经是一个ref 则直接返回if (isRef(rawValue)) {return rawValue}// 实例化 classreturn new RefImpl(rawValue, shallow)
}

class RefImpl<T> {private _value: Tprivate _rawValue: Tpublic dep?: Dep = undefined// 用于区分 ref 的不可枚举属性 例如 isRef 方法就是直接判断这个属性public readonly __v_isRef = true// 构造函数constructor(value: T, public readonly __v_isShallow: boolean) {this._rawValue = __v_isShallow ? value : toRaw(value)this._value = __v_isShallow ? value : toReactive(value)}get value() {// 依赖收集trackRefValue(this)return this._value}set value(newVal) { // 拿到原始值newVal = this.__v_isShallow ? newVal : toRaw(newVal)// 判断是否有变化 如有才进行更新if (hasChanged(newVal, this._rawValue)) {this._rawValue = newValthis._value = this.__v_isShallow ? newVal : toReactive(newVal)// 依赖更新triggerRefValue(this, newVal)}}
} 

2) toReactive()

我们日常使用的时候会发现, ref 传入一个对象 也能正常使用,其玄机就在 创建class 的时候,构造函数中调用了 toReactive 这个函数

export const toReactive = <T extends unknown>(value: T): T =>// 如果是一个对象则利用 reactive 代理成 Proxy 返回isObject(value) ? reactive(value) : value 

3)proxyRefs() 自动脱 ref

我们在使用 ref 的时候会发现,从 setup 返回的 ref, 在页面中使用并不需要 .value ,这都归功 proxyRefs 这个函数,减少了我们在模板中需要判断 ref 的心智负担

<template> // 这里并不需要 .value  // 并且如果我 直接在模板的点击事件中 使用 count++ 响应式也不会丢失<div @click="count++"> {{ count }} </div>
</template>

const myComponent = { setup() {const count = ref(0)return { count } }
} 

下面我们就来看看 proxyRefs 的实现

 export function proxyRefs<T extends object>(objectWithRefs: T
): ShallowUnwrapRef<T> { // 如果是 reactive 则不处理return isReactive(objectWithRefs)? objectWithRefs// 如果是 ref 则直接通过 Proxy 代理一下: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

export function unref<T>(ref: T | Ref<T>): T { // 如果是 ref 直接返回 .value 的值return isRef(ref) ? (ref.value as any) : ref
}

const shallowUnwrapHandlers: ProxyHandler<any> = {// get 的时候直接脱 refget: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), set: (target, key, value, receiver) => {const oldValue = target[key]// 如果旧值是 ref 而新值不是 ref 直接把 新值 替换 旧值 的.value属性if (isRef(oldValue) && !isRef(value)) {oldValue.value = valuereturn true} else {return Reflect.set(target, key, value, receiver)}}
} 

然后我们会发现在模板调用中,会自动把setup的返回值通过 proxyRefs 调用一遍

通过上面的源码来个总结:

  • 我们在编写 Vue 组件的时候, 组件中 setup 的函数所返回的数据自动传给 proxyRefs 函数处理一遍,所以我们在页面中使用 无需 .value
  • ref 最后在 模板中 还是被 Proxy 代理 了一遍

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值