淺談 Vue3 響應式原理與源碼解讀
一. 瞭解幾個概念
什麼是響應式
在開始響應式原理與源碼解析之前,需要先了解一下什麼是響應式?首先明確一個概念:響應式是一個過程,它有兩個參與方:
-
觸發方:數據
-
響應方:引用數據的函數
當數據發生改變時,引用數據的函數會自動重新執行,例如,視圖渲染中使用了數據,數據改變後,視圖也會自動更新,這就完成了一個響應的過程。
副作用函數
在Vue
與React
中都有副作用函數的概念,什麼是副作用函數?如果一個函數引用了外部的數據,這個函數會受到外部數據改變的影響,我們就說這個函數存在副作用,也就是我們所說的副作用函數。初聽這個名字不太好理解,其實 副作用函數就是引用了數據的函數或是與數據相關聯的函數。舉個例子:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<script>
const obj = {
name: 'John',
}
// 副作用函數 effect
function effect() {
app.innerHTML = obj.name
console.log('effect', obj.name)
}
effect()
setTimeout(() => {
obj.name = 'ming'
// 手動執行 effect 函數
effect()
}, 1000);
</script>
</body>
</html>
在上面例子中,effect
函數里面引用了外部的數據obj.name
, 如果這個數據發生了改變,則會影響到這個函數,類似effect
的這種函數就是副作用函數。
實現響應式的基本步驟
在上面的例子中, 當obj.name
發生了改變,effect
是我們手動執行的,如果能監聽到obj.name
的變化,讓其自動執行副作用函數effect
,那麼就實現了響應式的過程。其實無論是 Vue2
還是 Vue3
,響應式的核心都是 數據劫持/代理、依賴收集、依賴更新
,只不過由於實現數據劫持方式的差異從而導致具體實現的差異。
-
Vue2
響應式:基於Object.defineProperty()
實現的數據的劫持 -
Vue3
響應式:基於Proxy
實現對整個對象的代理
關於Vue2
的響應式這裏不做重點講解,這篇文章主要關注Vue3
響應式原理的實現。
二. Proxy 與 Reflect
在解析Vue3
的響應式原理之前,首先需要了解兩個 ES6 新增的 API:Porxy
與Reflect
。
Proxy
Proxy
: 代理,顧名思義主要用於爲對象創建一個代理,從而實現對對象基本操作的攔截和自定義。可以理解成,在目標對象之前架設一層 “攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。基本語法:
let proxy = new Proxy(target, handler);
-
target
: 需要攔截的目標對象 -
handler
: 也是一個對象,用來定製攔截行爲 舉個例子:
const obj = {
name: 'John',
age: 16
}
const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj與objProxy是否相等',obj === objProxy);
// 輸出
[Log] obj.age – 20
[Log] objProxy.age – 20
[Log] obj與objProxy是否相等 – false
這裏objProxy
的handler
爲空,則直接指向被代理對象, 並且代理對象與數據源對象並不全等. 如果需要更加靈活的攔截對象的操作,就需要在handler
中添加對應的屬性。例如:
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`獲取對象屬性${key}值`)
return target[key]
},
set(target, key, value, receiver) {
console.log(`設置對象屬性${key}值`)
target[key] = value
},
deleteProperty(target, key) {
console.log(`刪除對象屬性${key}值`)
return delete target[key]
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
// 輸出
[Log] 獲取對象屬性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 設置對象屬性age值 (example01.html, line 26)
[Log] 刪除對象屬性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)
上面的例子,我們在捕獲器中定義了set()
、get()
、deleteProperty()
屬性,通過對proxy
的操作實現了對obj
的操作攔截。這些屬性的觸發方法有如下參數:
-
target
—— 是目標對象,該對象被作爲第一個參數傳遞給new Proxy
-
key
—— 目標屬性名稱 -
value
—— 目標屬性的值 -
receiver
—— 指向的是當前操作 正確的上下文。如果目標屬性是一個getter
訪問器屬性,則receiver
就是本次讀取屬性所指向的this
對象。通常,receiver
這就是proxy
對象本身, 但是如果我們從proxy
繼承,則receiver
指的是從該proxy
繼承的對象 -
當然除了以上三個還有一些常用的屬性操作方法:
-
has()
,攔截:in 操作符. -
ownKeys()
, 攔截:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)
-
construct()
, 攔截:new
操作等
Reflect
Reflect
: 反射,就是將代理的內容反射出去。Reflect
與Proxy
一樣,也是 ES6
爲了操作對象而提供的新 API
。它提供攔截JavaScript
操作的方法,這些方法與Proxy handlers
提供的的方法是一一對應的,只要是Proxy
對象的方法,就能在Reflect
對象上找到對應的方法。且 Reflect
不是一個函數對象,即不能進行實例化,其所有屬性和方法都是靜態的。還是上面的例子
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`獲取對象屬性${key}值`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`設置對象屬性${key}值`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`刪除對象屬性${key}值`)
return Reflect.deleteProperty(target, key)
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
上面的例子中
-
Reflect.get()
代替target[key]
操作 -
Reflect.set()
代替target[key] = value
操作 -
Reflect.deleteProperty()
代替delete target[key]
操作 當然除了上面的方法還有一些常用的Reflect
方法:
Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
三. reactive、ref 源碼解析
瞭解了Proxy
與Reflect
,看下Vue3
是如何通過porxy
實現響應式的。其核心是下面要介紹的兩個方法:reactive
、ref
. 這裏依照 Vue3.2 版本的源碼進行解析。
reactive 的源碼實現
打開源文件,找到文件packages/reactivity/src/reactive.ts
查看源碼。
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
- 剛開始對
target
進行響應式只讀判斷,如果爲true
,則直接返回target
,reactive
實現的核心方法是createReactiveObject()
:
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
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
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
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
-
createReactiveObject()
方法有五個參數: -
target
: 傳入的原始目標對象 -
isReadonly
: 是否是隻讀的標識 -
baseHandlers
: 爲普通對象創建proxy
時的第二個參數handler
-
collectionHandlers
: 爲collection
類型對象創建proxy
時的第二個參數handler
-
proxyMap
:WeakMap
類型的map
,主要用於存儲target
與他的proxy
之間的對應關係
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
-
源碼可以看到,他將對象分爲
COMMON
對象(Object
和Array
)與COLLECTION
類型對象 (Map
、Set
、WeakMap
、WeakSet
), 這樣區分的主要目的是爲了根據不通的對象類型,來定製不同的handler
-
在
createReactiveObject()
的前幾行,進行了一系列的判斷: -
首先判斷
target
是否是對象,如果爲false
, 直接return
-
判斷
target
是否是響應式對象,如果爲true
, 直接return
-
判斷是否已經爲
target
創建過proxy
了,如果爲true
, 直接return
-
判斷
target
是否是剛纔上面提到的 6 種對象類型,如果爲false
, 直接return
-
如果以上條件都滿足,則爲
target
創建proxy
, 並return
這個proxy
接下來就是根據不同的對象類型,傳入不同的handler
的邏輯處理了,主要關注baseHandlers
,裏面存在五個屬性操作方法,這重點解析get
與set
方法。
源碼位置:
packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
get
與依賴收集
- 可以看到
mutableHandlers
裏面就是我們熟悉的各種鉤子函數。當我們對proxy
對象進行訪問或是修改時,調用相應的函數進行處理。首先看get
裏面是如何對訪問target
的副作用函數進行收集的:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} 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)) {
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
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
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
}
}
-
如果
key
值爲__v_isReactive
、__v_isReadonly
進行相應的返回,如果key==='__v_raw'
並且WeakMap
中key
爲target
的值不爲空,則返回target
-
如果
target
是數組,則 重寫 / 增強 數組對應的方法在這些方法裏面調用
track()
進行依賴收集 -
數組元素的查找方法:
includes、indexOf、lastIndexOf
-
修改原數組 的方法:
push、pop、unshift、shift、splice
-
對
Reflect.get()
方法的返回值,也就是當前數據對象的屬性值res
進行判斷,如果res
是普通對象且非只讀,則調用track()
進行依賴收集 -
如果
res
是淺層響應,直接返回,如果res
是ref
對象,則返回其value
值 -
如果
res
是 對象類型並且是只讀的,則調用readonly(res)
, 否則遞歸調用reactive(res)
方法 -
如果以上都不滿足,直接向外返回對應的 屬性值
那麼核心方法就是如果利用
track()
進行依賴收集的處理了, 源碼在 ``packages/reactivity/src/effect.ts`
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
-
首先進行是否正在進行依賴收集的判斷處理
-
const targetMap = new WeakMap<any, KeyToDepMap>()
創建一個targetMap
容器,用於保存和當前響應式對象相關的依賴內容,本身是一個WeakMap
類型 -
將對應的 響應式對象 作爲
targetMap
的 鍵,targetMap
的 Value 是一個depsMap
(屬於Map
實例),depsMap
存儲的就是和當前響應式對象的每一個key
對應的具體依賴 -
depsMap
的鍵是響應式數據對象的 key,Value 是一個deps
(屬於Set
實例),這裏之所以使用Set
是爲了避免副作用函數的重複添加,避免重複調用
以上就是整個 get() 捕獲器以及依賴收集的核心流程。
set
與依賴更新
我們在回到baseHandlers
中看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) {
value = toRaw(value)
oldValue = toRaw(oldValue)
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
}
const 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
}
}
-
首先進行舊值的保存
oldValue
-
如果不是淺層響應,
target
是普通對象,並且舊值是個響應式對象,則執行賦值操作:oldValue.value = value
, 返回true
,表示賦值成功 -
判斷是否存在對應 key 值
hadKey
-
執行
Reflect.set
設置對應的屬性值 -
判斷對象是原始原型鏈上的內容(非自定義添加),則不觸發依賴更新
-
根據目標對象不存在對應的 key, 調用
trigger
, 進行依賴更新
以上就是整個baseHandlers
關於依賴收集與依賴更新的核心流程。
ref 的源碼實現
我們知道ref
可以定義基本數據類型、引用數據類型的響應式。來看下它的源碼實現:packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly _shallow = false) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
-
從上面的函數調用流程可以看出,實現
ref
的核心就是實例化了一個RefImpl
對象。爲什麼這裏要實例化一個RefImpl
對象呢,其目的在於Proxy
代理的目標也是對象類型,無法通過爲基本數據類型創建proxy
的方式來進行數據代理。只能把基本數據類型包裝爲一個對象,通過自定義的get、set
方法進行 依賴收集 和 依賴更新 -
來看
RefImpl
對象屬性的含義: -
**_ value**:用於
保存ref當前值
,如果傳遞的參數是**對象**,它就是用於保存經過 **reactive 函數轉化後的值**,否則_value
與_rawValue
相同 -
**_ rawValue**:用於保存當前 ref 值對應的**原始值**,如果傳遞的參數是**對象**,它就是用於保存轉化前的原始值,否則
_value
與_rawValue
相同。這裏toRaw()
函數的作用就是將**的響應式對象轉爲普通對象** -
dep:是一個
Set
類型的數據,用來存儲當前的ref
值收集的依賴。至於這裏爲什麼用Set
上面我們有闡述,這裏也是同樣的道理 -
_v_isRef :標記位,只要被
ref
定義了,都會標識當前數據爲一個Ref
,也就是它的值標記爲true
-
另外可以很清楚的看到
RefImpl類
暴露給實例對象的get、set
方法是 value,所以對於ref
定義的響應式數據的操作我們都要帶上 .value -
如果傳入的值是對象類型,會調用
convert()
方法,這個方法裏面會調用reactive()
方法對其進行響應式處理 -
RefImpl
實例關鍵就在於trackRefValue(this)
和triggerRefValue(this, newVal)
的兩個函數的處理,我們大概也知道它們就是依賴收集、依賴更新, 原理基本與reactive
處理方式類似,這裏就不在闡述了
五. 總結
-
對於基礎數據類型只能通過
ref
來實現其響應式,核心還是將其包裝成一個RefImpl
對象,並在內部通過自定義的get value()
與set value(newVal)
實現依賴收集與依賴更新。 -
對於對象類型,
ref
與reactive
都可以將其轉化爲響應式數據,但其在ref
內部,最終還是會調用reactive
函數實現轉化。reactive
函數,主要通過創建了Proxy實例對象
,通過Reflect
實現數據的獲取與修改。
一些參考:
https://github.com/vuejs/vue
https://zh.javascript.info/proxy#reflect
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ixBZhPic9otUv9x66cTxzw