近期 Vue 官方正式開放了 3.x 的源碼,目前處於Pre Alpha階段,筆者出於興趣,抽空對 Vue 3.x 源碼的數據響應式部分作了簡單閱讀。本文經過分析 Vue 3.x 的 reactive
API 的原理,能夠更方便理解 Vue 3.x 比起 Vue 2.x 響應式原理的區別。javascript
在 Vue 3.x 源碼開放以前,筆者曾寫過Vue Composition API 響應式包裝對象原理, Vue 3.x 的 reactive
API 的實現與之有相似,感興趣的同窗能夠結合前文進行閱讀。html
閱讀此文以前,若是對如下知識點不夠了解,能夠先了解如下知識點:vue
筆者以前也寫過相關文章,也能夠結合相關文章:java
進入vue-next的項目倉庫,咱們能夠把 Vue 3.x 項目代碼都clone下來,能夠看到,經過執行vue-next/scripts/build.js能夠將 Vue 3.x 的代碼使用 rollup 打包,生成一個名爲vue.global.js
,可供開發者引用。爲了方便調試,咱們執行vue-next/scripts/dev.js,此時開啓 rollup 的 watch 模式,能夠方便咱們對源碼進行調試、修改、輸出。react
在項目目錄下新建一個test.html
,引用構建在項目目錄下的packages/vue/dist/vue.global.js
,在項目目錄下執行npm run dev
,寫一個最簡單 Vue 3.x 的 demo ,用瀏覽器打開能夠直接運行,利用這個 demo ,咱們構建好了 Vue 3.x 基本的運行環境,下面能夠開始進行源碼的調試了。git
<!DOCTYPE html>
<html>
<head>
<title>vue-demo</title>
</head>
<body>
<div id="app"></div>
<script src="./packages/vue/dist/vue.global.js"></script>
<script> const { createComponent, createApp, reactive, toRefs } = Vue; const component = createComponent({ template: ` <div> {{ count }} <button @click="addHandler">add</button> </div> `, setup(props) { const data = reactive({ count: 0, }); const addHandler = () => { data.count++; }; return { ...toRefs(data), addHandler, }; }, }); createApp().mount(component, document.querySelector('#app')); </script>
</body>
</html>
複製代碼
打開vue-next/packages/reactivity/src/reactive.ts,首先能夠找到reactive
函數以下:github
export function reactive(target: object) {
// 若是是readonly對象的代理,那麼這個對象是不可觀察的,直接返回readonly對象的代理
if (readonlyToRaw.has(target)) {
return target
}
// 若是是readonly原始對象,那麼這個對象也是不可觀察的,直接返回readonly對象的代理,這裏使用readonly調用,能夠拿到readonly對象的代理
if (readonlyValues.has(target)) {
return readonly(target)
}
// 調用createReactiveObject建立reactive對象
return createReactiveObject(
target, // 目標對象
rawToReactive, // 原始對象映射響應式對象的WeakMap
reactiveToRaw, // 響應式對象映射原始對象的WeakMap
mutableHandlers, // 響應式數據的代理handler,通常是Object和Array
mutableCollectionHandlers // 響應式集合的代理handler,通常是Set、Map、WeakMap、WeakSet
)
}
複製代碼
上面的代碼很好理解,調用reactive
,首先進行是不是 readonly 對象的判斷,若是 target
對象是 readonly 對象或者經過調用Vue.readonly
返回的代理對象,則是不可相應的,會直接返回 readonly 響應式代理對象。而後調用createReactiveObject
建立響應式對象。算法
createReactiveObject
傳遞的五個參數分別是:目標對象、原始對象映射響應式對象的WeakMap、響應式對象映射原始對象的WeakMap、響應式數據的代理handler,通常是Object和Array、響應式集合的代理handler,通常是Set、Map、WeakMap、WeakSet。咱們能夠翻到vue-next/packages/reactivity/src/reactive.ts最上方,能夠看到定義瞭如下常量:npm
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
複製代碼
能夠看到在reactive
中會預存如下四個WeakMap
:rawToReactive
、reactiveToRaw
、rawToReadonly
、readonlyToRaw
,分別是原始對象到響應式對象和 readonly 代理對象到原始對象的相互映射,另外定義了readonlyValues
、nonReactiveValues
,分別是 readonly 代理對象的集合與調用Vue.markNonReactive
標記爲不可相應對象的集合。collectionTypes
是Set
、 Map
、 WeakMap
、 WeakSet
的集合api
用 WeakMap 來進行相互映射的緣由是 WeakMap 的 key 是弱引用的。而且比起 Map , WeakMap 的賦值和搜索操做的算法複雜度均低於 Map ,具體緣由可查閱相關文檔。
下面來看createReactiveObject
:
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
// 若是不是對象,直接返回,開發環境下會給警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目標對象已是可觀察的,直接返回已建立的響應式Proxy,toProxy就是rawToReactive這個WeakMap,用於映射響應式Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 目標對象已是響應式Proxy,直接返回響應式Proxy,toRaw就是reactiveToRaw這個WeakMap,用於映射原始對象
if (toRaw.has(target)) {
return target
}
// 目標對象是不可觀察的,直接返回目標對象
if (!canObserve(target)) {
return target
}
// 下面是建立響應式代理的核心邏輯
// Set、Map、WeakMap、WeakSet的響應式對象handler與Object和Array的響應式對象handler不一樣
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 建立Proxy
observed = new Proxy(target, handlers)
// 更新rawToReactive和reactiveToRaw映射
toProxy.set(target, observed)
toRaw.set(observed, target)
// 看reactive的源碼,targetMap的用處目前還不清楚,應該是做者預留的還沒有完善的feature而準備的
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
複製代碼
看了上面的代碼,咱們知道createReactiveObject
用於建立響應式代理對象:
target
是不是對象類型,若是不是對象,直接返回,開發環境下會給警告toProxy
就是rawToReactive
這個WeakMap
,用於映射響應式ProxytoRaw
就是reactiveToRaw
這個WeakMap
,用於映射原始對象Set
、Map
、WeakMap
、WeakSet
的響應式對象handler與Object
和Array
的響應式對象handler不一樣,要分開處理rawToReactive
和reactiveToRaw
映射下面的重心來到了分析mutableCollectionHandlers
和mutableHandlers
,首先分析vue-next/packages/reactivity/src/baseHandlers.ts,這個handler用於建立Object
類型和Array
類型的響應式Proxy使用:
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
複製代碼
咱們知道,最重要的就是代理get
陷阱和set
陷阱,首先來看get
陷阱:
function createGetter(isReadonly: boolean) {
return function get(target: object, key: string | symbol, receiver: object) {
// 經過Reflect拿到原始的get行爲
const res = Reflect.get(target, key, receiver)
// 若是是內置方法,不須要另外進行代理
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 若是是ref對象,代理到ref.value
if (isRef(res)) {
return res.value
}
// track用於收集依賴
track(target, OperationTypes.GET, key)
// 判斷是嵌套對象,若是是嵌套對象,須要另外處理
// 若是是基本類型,直接返回代理到的值
return isObject(res)
// 這裏createGetter是建立響應式對象的,傳入的isReadonly是false
// 若是是嵌套對象的狀況,經過遞歸調用reactive拿到結果
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
複製代碼
Reflect.get
,拿到原始的get行爲track
來收集依賴res
結果是不是對象類型,若是是對象類型,再次調用reactive(res)來拿到結果,避免循環引用的狀況下面來看set
陷阱:
function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {
// 首先拿到原始值oldValue
value = toRaw(value)
const oldValue = (target as any)[key]
// 若是原始值是ref對象,新賦值不是ref對象,直接修改ref包裝對象的value屬性
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 原始對象裏是否有新賦值的這個key
const hadKey = hasOwn(target, key)
// 經過Reflect拿到原始的set行爲
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)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
// 沒有這個key,則是添加屬性
// 不然是給原始屬性賦值
// trigger 用於通知deps,通知依賴這一狀態的對象更新
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
複製代碼
oldValue
ref
對象,新賦值不是ref
對象,直接修改ref
包裝對象的value
屬性Reflect
拿到原始的set行爲,若是原始對象裏是否有新賦值的這個key,沒有這個key,則是添加屬性,不然是給原始屬性賦值trigger
通知deps
更新,通知依賴這一狀態的對象更新分析了mutableHandlers
,下面來分析mutableCollectionHandlers
,打開vue-next/packages/reactivity/src/collectionHandlers.ts,這個handler用於建立Set
、Map
、WeakMap
、WeakSet
的響應式Proxy使用:
// 須要監聽的方法調用
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size(this: IterableCollections) {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false)
}
// ...
function createInstrumentationGetter( instrumentations: Record<string, Function> ) {
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) =>
// 若是是`get`、`has`、`add`、`set`、`delete`、`clear`、`forEach`的方法調用,或者是獲取`size`,那麼改成調用mutableInstrumentations裏的相關方法
Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(mutableInstrumentations)
}
複製代碼
看上面的代碼,咱們看到mutableCollectionHandlers
只有一個get
陷阱,這是爲何呢?由於對於Set
、Map
、WeakMap
、WeakSet
的內部機制的限制,其修改、刪除屬性的操做經過set
、add
、delete
等方法來完成,是不能經過Proxy
設置set
陷阱來監聽的,相似於 Vue 2.x 數組的變異方法的實現,經過監聽get
陷阱裏的get
、has
、add
、set
、delete
、clear
、forEach
的方法調用,並攔截這個方法調用來實現響應式。
關於爲何
Set
、Map
、WeakMap
、WeakSet
不能作到響應式,筆者在why-is-set-incompatible-with-proxy找到了答案。
那麼咱們理解了由於Proxy
對於Set
、Map
、WeakMap
、WeakSet
的限制,與 Vue 2.x 的變異方法相似,經過攔截get
、has
、add
、set
、delete
、clear
、forEach
的方法調用來監聽Set
、Map
、WeakMap
、WeakSet
數據類型的修改。看get
、has
、add
、set
、delete
、clear
、forEach
等方法就輕鬆多了,這些方法與對象類型的get
陷阱、has
、set
等陷阱handler相似,筆者在這裏不作過多講述。
本文是筆者處於繼續對 Vue 3.x 相關動態的關注,首先,筆者講述瞭如何搭建一個最簡單的 Vue 3.x 代碼的運行和調試環境,而後對 Vue 3.x 響應式核心原理進行解析,比起 Vue 2.x , Vue 3.x 對於響應式方面全面擁抱了 Proxy API,經過代理初始對象默認行爲來實現響應式;reactive
內部利用WeakMap
的弱引用性質和快速索引的特性,使用WeakMap
保存了響應式代理和原始對象, readonly 代理和原始對象的互相映射;最後,筆者分析了響應式代理的相關陷阱方法,能夠知道對於對象和數組類型,是經過響應式代理的相關陷阱方法實現原始對象響應式,而對於Set
、Map
、WeakMap
、WeakSet
類型,由於受到Proxy
的限制,Vue 3.x 使用了劫持get
、has
、add
、set
、delete
、clear
、forEach
等方法調用來實現響應式原理。