Vue3響應(yīng)式方案及ref?reactive的區(qū)別詳解
一、前言
距離 Vue3 出來(lái)了已經(jīng)有一段時(shí)間了, 最近呢重溫了一下Vue3的響應(yīng)式方案,以及ref reactive的區(qū)別,相信你看完本文,也能對(duì) Vue3 響應(yīng)式的了解有所提高
在看源碼的過(guò)程中,時(shí)常會(huì)感到框架的設(shè)計(jì)之美,看起來(lái)也會(huì)感到賞心悅目, 這也是能堅(jiān)持把源碼看下去的動(dòng)力
二、新的方案
1. 緣由
已知在 Vue2 中, 響應(yīng)式原理一直是采用
Object.defineProperty來(lái)進(jìn)行,那這樣做有什么權(quán)限呢? 下面一一道來(lái)- 這個(gè)API, 只能攔截
get/set的屬性 - 如對(duì)象 新增 或者 刪除 了屬性,則無(wú)法監(jiān)聽(tīng)到改變
- 對(duì)于數(shù)組,若使用數(shù)組的原生方法改變數(shù)組元素的時(shí)候 也無(wú)法監(jiān)聽(tīng)到改變
- 這個(gè)API, 只能攔截
所以呢在Vue3中采用了
Proxy和Reflect搭配來(lái)代理數(shù)據(jù)
2. Proxy 和 Reflect
1) Proxy
既然Vue3中響應(yīng)式數(shù)據(jù)是基于 Proxy 實(shí)現(xiàn)的,那么什么是Proxy呢?
使用Proxy可以創(chuàng)建一個(gè)代理對(duì)象,它可以實(shí)現(xiàn)對(duì) 對(duì)象數(shù)據(jù) 的 代理, 所以它 無(wú)法對(duì)非對(duì)象值進(jìn)行代理,也就是為什么Vue3中對(duì)于非對(duì)象值要使用 ref 來(lái)進(jìn)行響應(yīng)式的原因 (后面講解ref的時(shí)候再細(xì)說(shuō))
- 代理是指 允許我們攔截并重新定義對(duì)一個(gè)對(duì)象的基本操作。 例如: 攔截讀取、 修改等操作.
const obj = {}
const newP = new Proxy(obj, {
// 攔截讀取
get(){/*...*/ },
// 攔截設(shè)置屬性操作
set(){/*...*/ }
})2) Reflect
說(shuō)完了Proxy, 接下來(lái)我們來(lái)說(shuō)說(shuō) Reflect
通過(guò)觀察 MDN 官網(wǎng)可以發(fā)現(xiàn), Reflect的方法 和 Proxy的攔截器方法 名字基本一致
那就出現(xiàn)了一個(gè)問(wèn)題,我們為什么要用 Reflect 呢?

主要還是它的第三個(gè)參數(shù),你可以理解為函數(shù)調(diào)用過(guò)程中的this,我們來(lái)看看它配合 Proxy 具體的用途吧
const obj = {
foo: 1,
// obj 中有一個(gè) getter屬性 通過(guò)this獲取foo的值
get getFoo() { return this.foo; }
};
const newP = new Proxy(obj,
{
// 攔截讀取
get(target, key) {
console.log('讀取', key); // 注意這里目前沒(méi)有使用 Reflect
return target[key];
},
// 攔截設(shè)置屬性操作
set(target, key, newVal) {
console.log('修改', key);
target[key] = newVal
}
})
obj.foo++
console.log(newP.getFoo); 執(zhí)行上面代碼你會(huì)發(fā)現(xiàn), 在 Proxy 中 get 攔截的中,只會(huì)觸發(fā)對(duì) getFoo 屬性進(jìn)行讀取的攔截, 而無(wú)法觸發(fā)在 getFoo 里面對(duì) this.foo 進(jìn)行讀取的攔截!
問(wèn)題就出現(xiàn)在 getFoo 這個(gè)getter里, 這里面的 this 在我們 未使用 Reflect 的時(shí)候指向它的原始對(duì)象,所以我們才無(wú)法通過(guò) Proxy 攔截到屬性讀取
只需修改一下上面代碼中 Proxy 里面的 get 攔截方法
// 攔截讀取
get(target, key, receiver) {
console.log('讀取', key);
return Reflect.get(target, key, receiver); // 使用 Reflect返回讀取的屬性值
},這下再執(zhí)行上面的例子,就會(huì)發(fā)現(xiàn)能正常對(duì) getFoo 里面的 foo 屬性進(jìn)行讀取的攔截。 因?yàn)檫@個(gè)時(shí)候的 this 已經(jīng)指向了代理對(duì)象 newP
以上呢,就是對(duì) Proxy 和 Reflect 的簡(jiǎn)易講解,接下來(lái)我們講講 Vue3 中的 reactive
3. reactive
看源碼會(huì)發(fā)現(xiàn),我們平時(shí)使用 reactive 的時(shí)候,會(huì)調(diào)用一個(gè) createReactiveObject 的方法
這個(gè)地方在:
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, // 普通對(duì)象的 handlers
mutableCollectionHandlers, // Set Map 等類(lèi)型的 handlers
reactiveMap
)
}1) createReactiveObject() 函數(shù)
其中主要是做一些前置判斷,然后建立響應(yīng)式地圖
WeakMap -> Map -> Set
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 若目標(biāo)數(shù)據(jù)是不是對(duì)象則直接返回
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 代表原始數(shù)據(jù)
// 或者是非響應(yīng)式數(shù)據(jù)就直接返回 原數(shù)據(jù)
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如已被代理則直接返回代理的這個(gè)對(duì)象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
// 只有在白名單中的類(lèi)型才可以被代理
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 建立代理 Proxy
const proxy = new Proxy(
target,
// 使用不同的 hanlders
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 存儲(chǔ)到響應(yīng)式地圖中
proxyMap.set(target, proxy)
return proxy
}2) mutableHandlers() 函數(shù) -> 對(duì)象類(lèi)型的 handlers
這個(gè)地方在:
packages\reactivity\src\baseHandlers
主要講講get和set
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(), // 讀取屬性
set: createSetter(), // 設(shè)置屬性
deleteProperty, // 刪除屬性
has, // 判斷是否存在對(duì)應(yīng)屬性
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
}
// 如果是數(shù)組要進(jìn)行一些特殊處理
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 重寫(xiě)數(shù)組的方法
// '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
}
// 如果非只讀屬性 才進(jìn)行依賴(lài)收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 淺層響應(yīng)則直接返回對(duì)應(yīng)的值
if (shallow) {
return res
}
// 如果是ref 則自動(dòng)進(jìn)行 脫ref
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 返回值是對(duì)象
// 如果是只讀就用 readonly 包裹返回?cái)?shù)據(jù)
// 否則則進(jìn)行遞歸深層包裹 reactive 返回 Proxy 代理對(duì)象
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)
}
// 如都不是上面的判斷 則返回這個(gè)數(shù)據(jù)
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 = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 是否有對(duì)于的key
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 修改對(duì)應(yīng)的值
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 若目標(biāo)是原型鏈上的內(nèi)容就不觸發(fā)依賴(lài)
if (target === toRaw(receiver)) {
// 這里主要是判斷是 新增屬性 還是修改屬性的操作
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 最終返回結(jié)果
return result
}
}3) mutableInstrumentations() 函數(shù) -> Map Set等類(lèi)型的 handlers
這個(gè)地方在:
packages\reactivity\src\collectionHandlers
其主要是為了解決 代理對(duì)象 無(wú)法訪(fǎng)問(wèn)集合類(lèi)型的屬性和方法
function createInstrumentations() {
// 主要就是代理了 Map Set等類(lèi)型的方法 具體實(shí)現(xiàn)各位可以去上面地址中的文件里查看
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
之前說(shuō)過(guò) Proxy 代理的必須是對(duì)象數(shù)據(jù)類(lèi)型,而非對(duì)象數(shù)據(jù)類(lèi)型 例如: string number 等等 則不能用其進(jìn)行代理, 所以有了 ref 的概念
聯(lián)想到上面說(shuō)的 reactive 和我們?nèi)粘J褂玫?.value 的形式, 是不是就認(rèn)為 ref 直接把原始數(shù)據(jù)包裹成對(duì)象 然后通過(guò) Proxy 進(jìn)行代理的呢?
最開(kāi)始我也以為是這樣,但是查看了源碼中發(fā)現(xiàn)其實(shí)并不是, 其實(shí)是創(chuàng)建 ref 的時(shí)候, 實(shí)例化了一個(gè) class -> new RefImpl(rawValue, shallow) ,然后通過(guò)自定義的 get set來(lái)進(jìn)行依賴(lài)收集和依賴(lài)更新
源碼地址:
packages\reactivity\src\ref
1) createRef()
export function ref(value?: unknown) {
// 調(diào)用創(chuàng)建方法
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
// 如果已經(jīng)是一個(gè)ref 則直接返回
if (isRef(rawValue)) {
return rawValue
}
// 實(shí)例化 class
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
// 用于區(qū)分 ref 的不可枚舉屬性 例如 isRef 方法就是直接判斷這個(gè)屬性
public readonly __v_isRef = true
// 構(gòu)造函數(shù)
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 依賴(lài)收集
trackRefValue(this)
return this._value
}
set value(newVal) {
// 拿到原始值
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 判斷是否有變化 如有才進(jìn)行更新
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 依賴(lài)更新
triggerRefValue(this, newVal)
}
}
}2) toReactive()
我們?nèi)粘J褂玫臅r(shí)候會(huì)發(fā)現(xiàn), ref 傳入一個(gè)對(duì)象 也能正常使用,其玄機(jī)就在 創(chuàng)建class 的時(shí)候,構(gòu)造函數(shù)中調(diào)用了 toReactive 這個(gè)函數(shù)
export const toReactive = <T extends unknown>(value: T): T => // 如果是一個(gè)對(duì)象則利用 reactive 代理成 Proxy 返回 isObject(value) ? reactive(value) : value 復(fù)制代碼
3)proxyRefs() 自動(dòng)脫 ref
我們?cè)谑褂?ref 的時(shí)候會(huì)發(fā)現(xiàn),從 setup 返回的 ref, 在頁(yè)面中使用并不需要 .value ,這都?xì)w功 proxyRefs 這個(gè)函數(shù),減少了我們?cè)谀0逯行枰袛?ref 的心智負(fù)擔(dān)
<template>
// 這里并不需要 .value
// 并且如果我 直接在模板的點(diǎn)擊事件中 使用 count++ 響應(yīng)式也不會(huì)丟失
<div @click="count++"> {{ count }} </div>
</template>
const myComponent = {
setup() {
const count = ref(0)
return { count }
}
}下面我們就來(lái)看看 proxyRefs 的實(shí)現(xiàn)
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
// 如果是 reactive 則不處理
return isReactive(objectWithRefs)
? objectWithRefs
// 如果是 ref 則直接通過(guò) 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 的時(shí)候直接脫 ref
get: (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 = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}然后我們會(huì)發(fā)現(xiàn)在模板調(diào)用中,會(huì)自動(dòng)把setup的返回值通過(guò) proxyRefs 調(diào)用一遍

通過(guò)上面的源碼來(lái)個(gè)總結(jié):
- 我們?cè)诰帉?xiě) Vue 組件的時(shí)候, 組件中 setup 的函數(shù)所返回的數(shù)據(jù)會(huì)自動(dòng)傳給 proxyRefs 函數(shù)處理一遍,所以我們?cè)陧?yè)面中使用 無(wú)需 .value
- ref 最后在 模板中 還是被 Proxy 代理 了一遍
三、 結(jié)語(yǔ)
以上呢就是對(duì) Vue3 的響應(yīng)式的方案解析了, 以及關(guān)于 reactive ref的區(qū)別相信你如果仔細(xì)看完了,也會(huì)心知肚明了
到此這篇關(guān)于Vue3響應(yīng)式方案及ref reactive的區(qū)別詳解的文章就介紹到這了,更多相關(guān)Vue3響應(yīng)式及ref reactive區(qū)別內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于element-ui中el-form自定義驗(yàn)證(調(diào)用后端接口)
這篇文章主要介紹了關(guān)于element-ui中el-form自定義驗(yàn)證(調(diào)用后端接口),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
vue路由第二次進(jìn)入頁(yè)面created和mounted不執(zhí)行問(wèn)題及解決
這篇文章主要介紹了vue路由第二次進(jìn)入頁(yè)面created和mounted不執(zhí)行問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
vue使用mpegts.js實(shí)現(xiàn)播放flv的直播視頻流
這篇文章主要為大家詳細(xì)介紹了vue如何使用mpegts.js實(shí)現(xiàn)播放flv的直播視頻流,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-01-01
基于 Vue.js 之 iView UI 框架非工程化實(shí)踐記錄(推薦)
為了快速體驗(yàn) MVVM 模式,我選擇了非工程化方式來(lái)起步,并選擇使用 Vue.js,以及基于它構(gòu)建的 iView UI 框架。本文給大家分享基于 Vue.js 之 iView UI 框架非工程化實(shí)踐記錄,需要的朋友參考下吧2017-11-11
Vue+OpenLayer為地圖添加風(fēng)場(chǎng)效果
這篇文章主要為大家展示了一個(gè)demo,即利用Vue和OpenLayer在地圖上面添加風(fēng)場(chǎng)效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-04-04
vue單頁(yè)開(kāi)發(fā)父子組件傳值思路詳解
這篇文章主要介紹了vue單頁(yè)開(kāi)發(fā)父子組件傳值思路詳解,本文是小編抽空整理的思路,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05
Vue3+Element-plus項(xiàng)目自動(dòng)導(dǎo)入報(bào)錯(cuò)的解決方案
vue3出來(lái)一段時(shí)間了,element也更新了版本去兼容vue3,下面這篇文章主要給大家介紹了關(guān)于Vue3+Element-plus項(xiàng)目自動(dòng)導(dǎo)入報(bào)錯(cuò)的解決方案,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
Vue如何實(shí)現(xiàn)分批加載數(shù)據(jù)
這篇文章主要介紹了Vue如何實(shí)現(xiàn)分批加載數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue利用draggable實(shí)現(xiàn)多選拖拽效果
這篇文章主要為大家詳細(xì)介紹了如何利用vue中的draggable插件實(shí)現(xiàn)多選拖拽效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

