最近剛開(kāi)始用 vue3灌砖,其中組合式 API 的 ref 和 reactive 兩者讓我有些困惑:
- 它們都返回響應(yīng)式的數(shù)據(jù)送漠,那么它們兩者的區(qū)別在哪活鹰?
- 它們的原理是怎樣的分冈?
于是就有了看源碼的想法胁镐,源碼是直接從 https://unpkg.com/vue@3/dist/vue.global.js 上保存下來(lái)的偎血。后續(xù)就可以在源碼上面調(diào)試學(xué)習(xí)啦。
結(jié)論
先說(shuō)結(jié)論6⑵(我知道很多朋友不喜歡看過(guò)程颇玷,只要結(jié)論。比如我自己 0,0)
- 參數(shù)
-
ref()
函數(shù)的參數(shù)既可以是原始類型(string就缆、number帖渠、boolean)也可以是對(duì)象類型(對(duì)象、數(shù)組竭宰、Set空郊、Map)。 - 如果將一個(gè)對(duì)象類型的數(shù)據(jù)賦值給
ref()
函數(shù)切揭,這個(gè)對(duì)象將通過(guò)reactive()
轉(zhuǎn)為具有深層次響應(yīng)式的對(duì)象狞甚。 -
reactive()
函數(shù)只有在接收對(duì)象類型是響應(yīng)式的。它也可以接收 ref 函數(shù)返回的對(duì)象伴箩,不過(guò)如果需要解構(gòu)就需要使用對(duì)象包裹入愧。如{ a: refObj }
-
- 返回值
-
ref()
接受一個(gè)內(nèi)部值,并返回一個(gè)響應(yīng)式的嗤谚、可更改的 ref 對(duì)象棺蛛。該對(duì)象通過(guò)內(nèi)部值.value
的 setter 和 getter 來(lái)獲取和修改內(nèi)部數(shù)據(jù),如count.value = 4
巩步。 -
reactive()
函數(shù)返回一個(gè)對(duì)象的深層次響應(yīng)式代理旁赊。
-
他們最終的目的都是能響應(yīng)式渲染模板(即數(shù)據(jù)變化后網(wǎng)頁(yè)內(nèi)容也隨之變化)。
ref
源碼
先看下 ref
的源碼椅野,ref()
函數(shù)執(zhí)行了 createRef()
函數(shù)终畅,而 createRef()
中實(shí)例化了 RefImpl
類。
function ref(value) {
return createRef(value, false)
}
function createRef(rawValue, shallow) {
// 如果已經(jīng)是 ref 則直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
在 RefImpl
類中除了構(gòu)造函數(shù)竟闪,只有一個(gè) value 內(nèi)部值的 setter 和 getter 函數(shù)离福。在構(gòu)造函數(shù)中 _rawValue
是原始數(shù)據(jù),而 _value
是響應(yīng)數(shù)據(jù)(如果數(shù)據(jù)是對(duì)象類型則為 Proxy)炼蛤。
那么 _value
是如何來(lái)的妖爷?如果不是淺層響應(yīng)式,則會(huì)調(diào)用 toReactive
函數(shù)理朋。
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow
this.dep = undefined
this.__v_isRef = true
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
在 toRaw()
函數(shù)中絮识,遞歸獲取數(shù)據(jù)的原始數(shù)據(jù)绿聘。(reactive()
函數(shù)返回的代理對(duì)象中帶有 __v_raw
標(biāo)簽,它會(huì)讓 getter 函數(shù)返回原始數(shù)據(jù))
function toRaw(observed) {
const raw = observed && observed['__v_raw' /* ReactiveFlags.RAW */]
return raw ? toRaw(raw) : observed
}
在 toReactive()
函數(shù)中次舌,就可以看到已經(jīng)使用 reactive()
函數(shù)的邏輯了熄攘。
如果將一個(gè)對(duì)象賦值給 ref,那么這個(gè)對(duì)象將通過(guò) reactive() 轉(zhuǎn)為具有深層次響應(yīng)式的對(duì)象彼念。
const toReactive = (value) => (isObject(value) ? reactive(value) : value)
順便瞅一眼 isObject()
函數(shù)挪圾,對(duì)象類型的判定就是 typeof val === 'object'
。不過(guò)由于 JavaScript 的缺陷逐沙,所以 typeof null
也是 object
洛史,需要排除掉。
const isObject = (val) => val !== null && typeof val === 'object'
小實(shí)驗(yàn)
實(shí)驗(yàn)出真知
const a = ref('123')
a.value += '456'
// '123456'
const b = ref(6)
b.value += 8
// 14
const c = ref(false)
c.value = !c.value
// true
const r3 = ref(false)
r3.value = true
r3.value = 'oh li gei' // value 是不限定類型的
// oh li gei
const d = ref(null)
// null
const e = ref(undefined)
// undefined
const f = ref(Symbol())
// Symbol()
// 這里打贏 ref 返回的對(duì)象
const g = ref({ a: [1, 2, 3] })
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
const h = ref([3, 4, 5, 6])
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
const i = ref(new Set())
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Set(1), _value: Proxy}
// 驗(yàn)證 toRaw 函數(shù)的 __v_raw 屬性
const x = reactive({ a: 1, b: { c: { d: 3 } } })
const y = ref(x)
console.log('x', x.__v_raw) // { a: 1, b: { c: { d: 3 } } }
console.log('y', y.__v_raw) // undefined
- 對(duì)于字符串酱吝、數(shù)字也殖、布爾類型來(lái)說(shuō),ref 函數(shù)可以讓這些數(shù)據(jù)變成響應(yīng)式的务热。
- 對(duì)于 null忆嗜、undefined、symbol 這類特殊數(shù)據(jù)崎岂,ref 函數(shù)返回值還是其本身捆毫,無(wú)意義。
- 對(duì)于對(duì)象類型數(shù)據(jù)冲甘,正如源碼所說(shuō)绩卤,對(duì)數(shù)據(jù)使用了 reactive() 來(lái)進(jìn)行深層響應(yīng)式代理。從 ref 返回的對(duì)象可以看出江醇,
_rawValue
是原始數(shù)據(jù)濒憋,而_value
是數(shù)據(jù)的代理。 -
reactive
函數(shù)返回的代理對(duì)象中帶有__v_raw
標(biāo)簽陶夜,會(huì)返回原始數(shù)據(jù)
reactive
源碼
reactive()
reactive()
函數(shù)除了判斷只讀外就只是調(diào)用了 createReactiveObject()
函數(shù)凛驮。
createReactiveObject()
函數(shù)中,排除了各種不需要代理的情況条辟,并根據(jù)數(shù)據(jù)類型不同進(jìn)行不同的代理邏輯處理黔夭。最后將代理結(jié)構(gòu)記錄到一個(gè) Map 中。
function reactive(target) {
// 如果 target 是 Readonly 的代理羽嫡,返回自身
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap,
) {
// target 不是對(duì)象類型本姥,返回自身
if (!isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target 已經(jīng)是代理,返回自身
if (
target['__v_raw' /* ReactiveFlags.RAW */] &&
!(isReadonly && target['__v_isReactive' /* ReactiveFlags.IS_REACTIVE */])
) {
return target
}
// target 已經(jīng)有響應(yīng)的代理杭棵,返回代理
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
/**
* 判斷 target 數(shù)據(jù)類型
* 0 無(wú)效婚惫,直接返回
* 1 COMMON 類型,使用 baseHandlers 代理配置
* 2 COLLECTION 類型,使用 collectionHandlers 代理配置
*/
const targetType = getTargetType(target)
if (targetType === 0 /* TargetType.INVALID */) {
return target
}
const proxy = new Proxy(
target,
targetType === 2 /* TargetType.COLLECTION */
? collectionHandlers
: baseHandlers,
)
// 記錄代理關(guān)系
proxyMap.set(target, proxy)
return proxy
}
數(shù)據(jù)類型判斷
判斷數(shù)據(jù)類型的代碼如下辰妙,根據(jù)不同的數(shù)據(jù)分為:
- 0 無(wú)效數(shù)據(jù)類型,不進(jìn)行代理返回自身甫窟。
- 1 普通對(duì)象類型
- 2 收集器類型
類型的獲取是通過(guò) Object.prototype.toString.call(target)
獲取到 '[object Set]'
這類字符串密浑,并截取 Set
這段有效字符串返回。
function getTargetType(value) {
return value['__v_skip' /* ReactiveFlags.SKIP */] ||
!Object.isExtensible(value)
? 0 /* TargetType.INVALID */
: targetTypeMap(toRawType(value))
}
function targetTypeMap(rawType) {
switch (rawType) {
case 'Object':
case 'Array':
return 1 /* TargetType.COMMON */
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return 2 /* TargetType.COLLECTION */
default:
return 0 /* TargetType.INVALID */
}
}
普通類數(shù)據(jù)代理
在 reactive()
函數(shù)中可以看到粗井,代理配置分別使用的是 mutableHandlers 和 mutableCollectionHandlers尔破。
普通類型的代理配置 mutableHandlers 如下。這里代碼量較大浇衬,暫時(shí)就只貼出 getter 和 settter 函數(shù)懒构。
const mutableHandlers = {
get: get$1, // get 方法用于攔截某個(gè)屬性的讀取操作
set: set$1, // set 方法用來(lái)攔截某個(gè)屬性的賦值操作
deleteProperty, // deleteProperty 方法用于攔截 delete 操作
has: has$1, // has() 方法用來(lái)攔截HasProperty操作
ownKeys, // ownKeys() 方法用來(lái)攔截對(duì)象自身屬性的讀取操作
}
在 createGetter 中的處理邏輯如下:
- 如果是標(biāo)簽
__v_raw
等則返回響應(yīng)的值; - 如果是數(shù)組 API 的關(guān)鍵字
inclueds
push
等就用 arrayInstrumentations 進(jìn)行處理耘擂;(所以可以在 reactive 的返回值中直接使用數(shù)組 APIarr.push()
) - 通過(guò)
Reflect.get()
獲取到目標(biāo)返回值胆剧。 - 如果返回值是一個(gè)對(duì)象,且不是只讀數(shù)據(jù)醉冤。那么就以遞歸的方式對(duì)這個(gè)子對(duì)象使用
reactive()
函數(shù)繼續(xù)綁定響應(yīng)式代理秩霍。(即深層響應(yīng)式轉(zhuǎn)換) - 返回最終結(jié)果。
const get$1 = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
return !isReadonly
} else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
return isReadonly
} else if (key === '__v_isShallow' /* ReactiveFlags.IS_SHALLOW */) {
return shallow
} else if (
key === '__v_raw' /* ReactiveFlags.RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver) // Reflect.get 方法查找并返回target對(duì)象的name屬性
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, 'get' /* TrackOpTypes.GET */, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref 解構(gòu)取值
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// 遞歸生成響應(yīng)式代理
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
在 set 函數(shù)中
- 首先是調(diào)用
toRaw()
函數(shù)將 value 和 oldValue 遞歸從代理變?yōu)樵紨?shù)據(jù)蚁阳。原理大致如reactive({ a: 1 }).__v_raw // output: { a: 1}
- 如果 oldValue 是 ref() 函數(shù)返回的铃绒,則進(jìn)行解構(gòu)賦值。
- 通過(guò) Reflect.set() 函數(shù)對(duì)代理目標(biāo) target 進(jìn)行賦值螺捐。
const set$1 = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
let oldValue = target[key]
// 不需要更新的情況
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue) // 遞歸轉(zhuǎn)為原始數(shù)據(jù)
value = toRaw(value) // 遞歸轉(zhuǎn)為原始數(shù)據(jù)
}
// ref 解構(gòu)賦值
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver) // Reflect.set 方法設(shè)置 target 對(duì)象的 name 屬性等于value颠悬。
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
}
}
return result
}
}
收集器類數(shù)據(jù)代理
收集器類型的代理配置只有一個(gè) getter 函數(shù),它對(duì)收集器類型數(shù)據(jù)的 API 進(jìn)行了定義定血。
如果調(diào)用 set.add()
map.get()
這類 API赔癌,就會(huì)去調(diào)用 instrumentations 對(duì)象中相應(yīng)的函數(shù)。否則就返回代理目標(biāo) target 自身澜沟。
const mutableCollectionHandlers = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false),
}
function createInstrumentationGetter(isReadonly, shallow) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (target, key, receiver) => {
if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
return !isReadonly
} else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
return isReadonly
} else if (key === '__v_raw' /* ReactiveFlags.RAW */) {
return target
}
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver,
)
}
}
const mutableInstrumentations = {
get(key) {
return get(this, key)
},
get size() {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false),
}
function add(value) {
value = toRaw(value) // 去代理
const target = toRaw(this) // 去代理
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value) // 執(zhí)行原生函數(shù)
trigger(target, 'add' /* TriggerOpTypes.ADD */, value, value)
}
return this
}
function set(key, value) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
}
return this
}
源碼小結(jié)
- reactive 函數(shù)只對(duì)
Object
Array
Map
Set
WeakMap
WeakSet
類型的數(shù)據(jù)生效届榄,且分為了兩種處理方式。 - reactive 函數(shù)會(huì)排除各種不符合條件的數(shù)據(jù)倔喂,返回?cái)?shù)據(jù)本身铝条。
- reactive 函數(shù)是通過(guò)代理 Proxy 實(shí)現(xiàn)數(shù)據(jù)的存取的。
- reactive 中的
__v_raw
__v_isShallow
并不是屬性值席噩,而是判斷標(biāo)簽班缰。會(huì)根據(jù)標(biāo)簽返回相應(yīng)結(jié)果。 - reactive 對(duì)于 ref 對(duì)象的解構(gòu)其實(shí)就是在 get 的時(shí)候取
.value
值悼枢,而在 set 的時(shí)候?qū)⒅祩鹘o.value
埠忘。 - reactive 對(duì)于 Set、Map 這類數(shù)據(jù),僅提供了 getter 方法莹妒。如果調(diào)用這類數(shù)據(jù) API 函數(shù)名船,vue 在做了數(shù)據(jù)處理后會(huì)去調(diào)用它的原生函數(shù)。如果是獲取數(shù)據(jù)內(nèi)容旨怠,則直接返回?cái)?shù)據(jù)本身渠驼。
- 對(duì)象類型數(shù)據(jù)想要變成響應(yīng)式的,就必須用 reactive 函數(shù)代理鉴腻。
- 上面代碼中用到了 ES6 的 Reflect 和 Proxy 迷扇,關(guān)于它們的更多內(nèi)容可以訪問(wèn) Proxy - ECMAScript 6 入門 和 Reflect - ECMAScript 6 入門 了解。
小實(shí)驗(yàn)
以下寫法 reactive() 返回值是其自身爽哎,但不是響應(yīng)式的蜓席。而且 vue 會(huì)發(fā)出警告:value cannot be made reactive: 123
var a = reactive('123')
// 123
function add() {
a += '456' // 變量 a 有變化,但是 HTML 無(wú)變化
}
var b = reactive(6)
b += 8
// 16
const c = reactive(false)
// false
setTimeout(() => {
c = true // c 變?yōu)?true课锌,但是 HTML 無(wú)變化
}, 1000)
const d = reactive(null)
// null
const e = reactive(undefined)
// undefined
const f = reactive(Symbol())
// Synbol()
下面這些情況可以正常使用 reactive()
函數(shù)厨内。
const g = reactive({ a: 1, b: { c: 3 } })
g.a++
// Proxy: { a: 2, b: { c: 3 } }
setInterval(() => {
// 網(wǎng)頁(yè)會(huì)每秒變化數(shù)據(jù)
g.a++
g.b.c += 2
}, 1000)
const h = reactive([3, 4, 5, 6])
h.push(8)
// Proxy: {0: 3, 1: 4, 2: 5, 3: 6, 4: 8}
const i = reactive(new Set())
i.add('2')
i.add({ b: 3 })
i.add(321)
// Proxy: { "Set(4)": [ "2", { "b": 3 }, 321 ] }
setTimeout(() => {
i.add(null)
// Proxy: { "Set(4)": [ "2", { "b": 3 }, 321, null ] }
}, 1000)
const j = reactive(new Map())
j.set('yo', 'good')
j.set('x', { b: 3 })
setTimeout(() => {
j.delete('x')
// Proxy: { "Map(1)": { "yo =>": "good" } }
}, 1000)
既然 reactive 函數(shù)可以解構(gòu) ref,那么進(jìn)行一些嘗試渺贤。以下是官網(wǎng)的原話隘庄。
值得注意的是,當(dāng)訪問(wèn)到某個(gè)響應(yīng)式數(shù)組或 Map 這樣的原生集合類型中的 ref 元素時(shí)癣亚,不會(huì)執(zhí)行 ref 的解包丑掺。
但在實(shí)際實(shí)驗(yàn)下來(lái)發(fā)現(xiàn)這句話并不嚴(yán)謹(jǐn)。
var a = ref(new Map())
var b = reactive(a)
a.value.set('a', 1)
b.value.set('b', 2) // 需要加上 value
// { "Map(2)": { "a =>": 1, "b =>": 2 } }
console.log('a === b', a.value === b.value) // true
var a = ref(new Set())
var b = reactive({ a })
a.value.add(1)
b.a.add(2) // ! 被對(duì)象包裹的 Map 是可以被解構(gòu)的
// { "Map(2)": { "a =>": 1, "b =>": 2 } }
console.log('a === b', a.value === b.a) // true
嘗試了 Object述雾、Array街州、Set 后發(fā)現(xiàn),被 ref 函數(shù)返回的對(duì)象如果直接傳給 reactive 函數(shù)是不會(huì)被解構(gòu)的玻孟,但如果 ref 對(duì)象被對(duì)象符號(hào)包裹 reactive({ ref: ref(new Set()) })
的情況下是可以被解構(gòu)的唆缴。
最后
本文我們先提出了 ref 和 reactive 的疑問(wèn),然后給出結(jié)果黍翎。再?gòu)脑创a層面逐步分析了 ref 和 reactive 函數(shù)面徽。也算是基本掌握其原理了。
關(guān)于 ref 和 reactive 的內(nèi)容就這么多啦匣掸,希望對(duì)你有用趟紊。
本文正在參加「金石計(jì)劃」