淺談 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