前言
用了很長時間的 componsition-api
了,最近想看看源碼,抱著單純的學習心態先從 reactive
開始吧。
個人習慣:
- 看代碼要帶著問題去看,不要盲目的去看
- 問題就是這次看源碼的主線,要圍繞著主線去展開,過程中和主線沒有多大關系的該忽略掉就忽略掉
- 開源項目一般都封裝的比較好,有可能一個函數中會引用多個文件中的函數,每次跳轉的時候將跳轉的目錄記下來,避免跳著跳著就不知道跳哪去了
- 進入到一個文件中先將函數都收起來,便于查看
前置準備
-
把
vue-next
從 github 上把項目clone
下來。 -
通過
yarn install
安裝依賴。 -
將
package.json
中dev
腳本增加sourcemap
最終命令為:"dev": "node scripts/dev.js --sourcemap"
。 -
因為
vue3
是通過rollup
打包的,所以還需要安裝rollup
。 -
執行
npm run dev
在 package/vue/ 目錄下會生成一個dist
文件夾。 -
在 package/vue/examples/ 目錄下新建
init.html
文件。<!DOCTYPE html> <html lang="en"> <body><div id="app"><h1>hello</h1></div><script src="../dist/vue.global.js"></script><script>const { watch, watchEffect, createApp, reactive } = Vuedebuggerconst data = reactive({a: 1,b: 2,count: 0})</script> </body> </html>
-
然后將這個文件以服務的形式跑起來進入
debug
,通過斷點可以進入到reactive
函數。
數據響應式 Reactive
reactive
函數一開始就判斷了如果傳入的 target
如果是一個只讀的對象則 return target
。
緊接著調用了 createReactiveObject
函數,先來看一下函數對應的參數:
target
將要被代理的對象- 是否只讀
baseHandlers
和collectionHandlers
都是proxy
的handler
,對應的實參分別是mutableHandlers
和mutableCollectionHandlers
,根據target
類型來決定proxy
的handler
reactiveMap
是當前文件中聲明的一個常量,用于存儲依賴,現在只需要它是一個weakMap
類型數據就好
函數一開始對 target
做了幾種情況的判斷,針對不同情況做了對應的 return
。
主要關注點是這一段代碼
const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
這里會根據 target
的類型來選擇對應的 handler
,targetType
是調用 getTargetType
函數的返回值,targetType
為 Map/Set/WeakMap/WeakSet
的時候會將 collectionHandlers
傳入 Proxy
,反之采用 baseHandlers
。
baseHandlers
baseHandlers
對應的 mutableHandlers
get
get
對應的 createGetter
的返回值
createGetter
接收兩個參數 isReadonly
和 shallow
,在 get
方法中先判斷了三種特殊情況,針對每種不同情況 return 了不同的值,其中在這幾個判斷中用到的 shallowReadonlyMap/readonlyMap/shallowReactiveMap
它們的類型和 reactiveMap
的類型都一樣都是 weakMap
,只不過是對應不同的狀態。
下邊判斷了是不是數組,如果是數組并且 isReadonly
為 false
且 key
是 includes/indexOf/lastIndexOf/push/pop/shift/unshift/splice
其中的一個的話則執行
return Reflect.get(arrayInstrumentations, key, receiver)
也就是對上述那些方法進行了重寫。這段代碼是為了解決邊緣情況
includes/indexOf/lastIndexOf
是為了避免產生以下情況const obj = {} const arr = reactive([obj]) arr.indexOf(obj) // -1 正常情況下應該返回的是 0
push/pop/shift/unshift/splice
是為了避免這些改變數組長度的方法在某些情況下進入死循環
再往下獲取了 key
在 target
中對應的值
const res = Reflect.get(target, key, receiver)
接著判斷 key
是 symbol
的情況。如果 isReadonly
為 false
則調用 track
函數進行依賴收集。
track
這個函數往后放一下,看完這個 get
函數后再回頭來看 track
。
接著又判斷了 createGetter
傳入的 shallow
為 true
和 res
為 ref
類型的這兩種情況。
如果 res
還是一個對象并且 isReadonly
為 false
則遞歸調用 reactive
函數,反之調用 readonly
函數。
從調用 reactive
函數這里可以看出,vue3
中的響應式和 vue2
的差別不僅在 defineProperty
和 proxy
上,在處理響應式的時機上也有變化,defineProperty
是一上來就將 target
上的所有屬性都變成響應式的,但是 vue3
是在當你去讀取這個 key
的時候,采取將 key
對應的 value 轉換成響應式的。
接下來去看一下 track
函數,這個函數的作用是用來收集依賴的。
一開始是一個判斷條件,接下來的這段代碼是為了獲取當前 key
對應的數據同時構造一個數據結構
let depsMap = targetMap.get(target)
if (!depsMap) {targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {depsMap.set(key, (dep = new Set()))
}
構造出來的數據結構是這樣的
targetMap = {target: {key: [dep,...] // dep, set 類型} // map 類型
} // weakMap 類型
后邊將 activeEffect
添加到 dep
中。這里的 activeEffect
其實就是我們要收集的依賴。
后邊又將 dep
添加到了 activeEffect
的 deps
中,這一塊當時在看的時候并不明白,后來是查資料明白的,我們放到后邊說。如果是開發環境還調用了 onTrack
函數,這個函數在文檔中有說,用于調試偵聽器的行為。
至此,track
函數就結束了。
遇到的問題
-
activeEffect.deps.push(dep)
是什么,為什么要這樣做語義上來看是將當前
key
所對應的dep
push
到了activeEffect
的deps
中,但是為什么要這樣做還有待思考。網上查了一番,這篇文章寫的還是很不錯的,對這個問題點也有一定的解析。文章內的總結: 這個操作是在為在
reactiveEffect
方法中提到的cleanup
方法做準備,每次收集activeEffect
之前,會先將activeEffect
中的deps
清空,然后再進行收集依賴。先清空再收集就是為了避免如果activeEffect
中有在特定條件下才會觸發的依賴收集,之前已經收集過了,但是這次不需要收集,所以會先把之前收集的清空掉,然后再針對當前這次需要收集的依賴進行收集,保證當前收集的依賴肯定是當前需要被收集的。這里又引申除了reactiveEffect
方法,這個方法在watchEffect
中會用到,本文不涉及,所以不展開說。set
set
對應的 createSetter
的返回值
createSetter
接收一個參數 shallow
用來表示是不是淺層對象,然后直接 return
了 set
函數。
首先判斷了不是淺層對象則處理 oldValue
是 ref
類型,但 newValue
不是 ref
類型的情況,因為 ref
類型的數據已經是響應式的了,所以不需要再次通過 trigger
函數來再次觸發依賴。
ref
和reactive
差不多,都可以返回一個響應式的數據,但是ref
需要通過.value
的形式來獲取值。
后邊的代碼就是判斷 target
是不是數組,用 Reflect.set()
將 value
添加到 target
中,后續又判斷了 key
在不在 target
中,如果 key
不在 target
中則按 ADD
類型調用 trigger
函數觸發依賴,反之以 SET
類型調用 trigger
函數。調用完之后將 Reflect.set()
的返回值 return
出去。
接下來看一下 trigger
函數
從 targetMap
中獲取 target
對應的依賴,沒有則 return
。
定義了 set
類型的 effects
和 add
方法,add
方法主要是將傳入的 deps
遍歷,然后將每個 effect
添加到 effects
中,這樣 effects
中就保存了所有待執行的 effect
。
下邊就是根據傳入的 type
來執行對應的 add
函數。
再往下就定義了 run
函數,它負責來執行 effect
,如果是開發環境 effect
中有 onTrigger
方法的話會先執行 onTrigger
方法,這個方法和 track
函數中調用的 onTrack
函數是一個意思,文檔中有說,用于調試偵聽器的行為。如果 effect
中有調度器的話會選擇用調度器來執行 effect
否則直接執行 effect
。
run
函數的定義完了就是遍歷 effects
,將 run
函數傳入并執行。
以上就是 trigger
函數的所有了。
deleteProperty
這個方法就沒什么好說的了,用 Reflect.deleteProperty()
執行刪除,判斷這個 key
是 target
自身的則調用 trigger
觸發依賴,然后 return
Reflect.deleteProperty()
的返回值。
has
執行 Reflect.has()
,在 key
是非 Symbol
類型的時候調用 track
函數收集依賴,然后 return
Reflect.has()
的返回值。
ownKeys
調用 track
收集依賴,return Reflect.ownKeys(target)
。
collectionHandler
collectionHandlers
對應的 mutableCollectionHandlers
get
get
對應的 createInstrumentationGetter
的返回值
createInstrumentationGetter
函數定義了 isReadonly
和 shallow
兩個參數,這里在調用的時候傳入的都是 false
。
方法內也根據這兩個參數去獲取了對應的 handlers
定義為 instrumentations
,后邊判斷了 key
的三種特殊情況,這里的 key
代表的是調用 get
方法時的 key
,根據 key
的不同 return 對應的值。
最后判斷 key
是不是 instrumentations
自身的屬性,如果是則 Reflect.get()
傳入的第一個值是 instrumentations
,反之直接將 target
,最后將 Reflect.get()
的返回值 return。
這個 collectionHandlers
不進行具體展開,主要還是圍繞著我們的主題進行。
collectionHandlers
到此結束。
總結
-
vue3
的響應式將target
對象傳入proxy
,然后利用handlers
,在執行get/has/ownKeys
等獲取值方法的時候進行依賴收集,在執行set/deleteProperty
等更改值方法的時候進行觸發依賴。 -
vue3
對于target
的某一個key
的value
值還是對象的時候,只有在讀取到這個key
的時候才將value
進行響應式處理,而vue2
的處理是初始化的時候直接將所有屬性及屬性值進行響應式處理。以上就是我關于
reactive
源碼的閱讀過程,在我最終讀完之后去跑 demo 的時候發現進入到track
函數的時候,在一開始那個判斷的地方就被return
出去了,并沒有真正的收集依賴。這也就產生了我的另外一個問題:什么時候才會收集依賴呢。有了這個問題,可以繼續把源碼看下去了。
參考鏈接
- 源碼系列:Vue3深入淺出(一)
- Vue3 文檔
- Vue3最啰嗦的Reactivity數據響應式原理解析