Mobx 源碼解讀(四) Reaction
Reaction 是一類的特殊的 Derivation,可以註冊響應函數,使之在條件滿足時自動執行。常用於觸發副作用,比如列印日誌、更新 DOM 或者發送網路請求。
Reaction 的生命周期可以用下圖表示:
- Reaction 在創建之後初次執行或依賴過期時,會加入到全局的 pendingReactions 隊列中
- pendingReactions 中的 Reaction 重新執行
- onInvalidate 調用,執行響應函數。
- 響應函數執行,同時收集依賴,即「運行時依賴收集」
使用 autorun, when 等 api,創建 Reaction 後,會立即執行該 Reaction 的 schedule 方法。第二篇中提到,Observable 在 reportChanged 時,也會調用這個方法將 Reaction 加入到全局的待執行隊列中:
schedule() {n if (!this._isScheduled) {n this._isScheduled = truen globalState.pendingReactions.push(this)n runReactions()n }n}n
與之前不同,這時的 runReactions 會立即執行 pendingReactions 中的所有 Reaction:
function runReactions() {n // 此時不在事務當中,且沒有 Reaction 正在執行n if (globalState.inBatch > 0 || globalState.isRunningReactions) returnn reactionScheduler(runReactionsHelper)n}n
Reaction 初次執行時會第一次收集依賴,此後當依賴發生變化時,它被加入 pendingReactions 中,並在下一次事務結束時重新執行,更新依賴。接下來看看 Reaction 執行過程中是如何完成依賴的收集和更新的:
Reaction 執行過程
runReactionsHelper 依次執行 pendingReactions 中所有的 Reaction:
// 設定 Reaction 計算的最大迭代次數,避免 Reaction 重新觸發自身造成死循環nconst MAX_REACTION_ITERATIONS = 100nn// 實際執行 Reaction 重新計算的函數nfunction runReactionsHelper() {n globalState.isRunningReactions = truen const allReactions = globalState.pendingReactionsn let iterations = 0nn // 當執行 Reaction 時,可能觸發新的 Reaction(Reaction 內允許設置 Observable的值),加入到 pendingReactions 中n // 所以這裡使用了兩層循環,外層檢查確保新加入的 Reaction 也會得到執行n // 內層每次清空當前 Reaction 數組,并迭代處理當前所有 Reactionn while (allReactions.length > 0) {n // 限制最大迭代次數n if (++iterations === MAX_REACTION_ITERATIONS) {n console.error(n `Reaction doesnt converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +n ` Probably there is a cycle in the reactive function: ${allReactions[0]}`n )n allReactions.splice(0) // 清空 pendingReactions 數組n }n // 清空 pendingReactions 數組,遍歷所有 pendingReactionsn let remainingReactions = allReactions.splice(0) n for (let i = 0, l = remainingReactions.length; i < l; i++)n // 依次調用 runReaction 方法n remainingReactions[i].runReaction()n }n globalState.isRunningReactions = falsen}n
pendingReactions 中的 Reaction 依次調用 runReaction 方法:
runReaction() {n if (!this.isDisposed) {n startBatch()n // 「是否待重新計算」的標誌置為 falsen this._isScheduled = falsen // 根據 dependenciesState 判斷是否需要重新計算,-1,0,2三種狀態好判斷,n // 計算值特有的1狀態參看本系列第三篇文章n if (shouldCompute(this)) {n // 「正在收集依賴過程中」的標誌置為 truen this._isTrackPending = truenn // 構造時傳入的 onInvalidaten this.onInvalidate()n // 通知 spyn if (this._isTrackPending && isSpyEnabled()) {n spyReport({n object: this,n type: "scheduled-reaction"n })n }n }n endBatch()n }n}n
注意這裡調用了 onInvalidate,該函數可以在響應函數執行之前做一些判斷,控制響應函數執行的「時機」。
Mobx 提供了 autorun, when, autorunAsync 等 api 用於創建 Reaction,它們的區別就在於構造 Reaction 時傳入的 onInvalidate 函數不同:
不同類型的 Reaction
autorun
autorun 的 onInvalidate 會直接執行 Reaction 的 track 方法,也就是說,只要 Reaction 執行,就會執行響應函數:
function autorun(arg1: any, arg2: any, arg3?: any) {n let name: string, view: (r: IReactionPublic) => any, scope: anyn // 參數處理...nn // 可以提供 this 值作為第三個參數n if (scope) view = view.bind(scope)nn // 第二個參數即`onInvalidate`n // onInvalidate 未進行任何判斷,直接調用 track 方法n const reaction = new Reaction(name, function() {n this.track(reactionRunner)n })nn // view 即我們傳入的函數,可以拿到 Reaction 實例作為參數n function reactionRunner() {n view(reaction)n }nn // 調用 schedule 方法,第一次執行並收集依賴n reaction.schedule()nn // 返回該 Reaction 的 Disposern return reaction.getDisposer()n}n
autorunAsync
autorunAsync(action: () => void, minimumDelay?: number, scope?)
節流版 autorun,在 minimumDelay 時間內只會執行一次響應函數。通過在 onInvalidate 內做節流處理來實現:
function autorunAsync(arg1: any, arg2: any, arg3?: any, arg4?: any) {n let name: string, func: (r: IReactionPublic) => any, delay: number, scope: anyn // 參數處理...nn if (scope) func = func.bind(scope)nn // 節流處理n let isScheduled = falsen const r = new Reaction(name, () => {n if (!isScheduled) {n isScheduled = truen setTimeout(() => {n isScheduled = falsen if (!r.isDisposed) r.track(reactionRunner)n }, delay)n }n })nn function reactionRunner() {n func(r)n }nn r.schedule()n return r.getDisposer()n}n
when
when(debugName?, predicate: () => boolean, effect: () => void, scope?)
when 在 predicate 函數返回 true 時執行,通過在 onInvalidate 進行這個判斷來實現。另外,它只執行一次就銷毀,所以它的 onInvalidate 不調用 track 去收集依賴,直接執行響應函數:
function when(arg1: any, arg2: any, arg3?: any, arg4?: any) {n let name: string, predicate: () => boolean, effect: Lambda, scope: anyn // 參數處理...nn const disposer = autorun(name, r => {n // 滿足 predicaten if (predicate.call(scope)) {n r.dispose()n const prevUntracked = untrackedStart()n // 直接執行 effectn ;(effect as any).call(scope)n untrackedEnd(prevUntracked)n }n })n return disposern}n
運行時依賴收集
Reaction 的 track 方法會執行響應函數並進行依賴收集,也就是「運行時依賴收集」的過程。
track 方法本身只是做了開始一個新事務,一些標誌屬性的修改和通知 spy 等工作。
運行時依賴收集的核心步驟在 trackDerivedFunction 函數中:
// 執行響應函數,同時收集依賴nfunction trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context) {n // 將該 Derivation 的 dependenciesState 和當前所有依賴的 lowestObserverState 設為最新n changeDependenciesStateTo0(derivation)nn derivation.newObserving = new Array(derivation.observing.length + 100)n // 記錄新的依賴的數量n derivation.unboundDepsCount = 0n // 每次執行都分配一個 uidn derivation.runId = ++globalState.runIdn // 當前 Derivation 記錄到全局的 trackingDerivation 中,這樣被觀察的 Observable 在其 reportObserved 方法中n // 就能獲取到該 Derivationn const prevTracking = globalState.trackingDerivationn globalState.trackingDerivation = derivationn let resultn try {n // 執行響應函數,收集使用到的所有依賴,加入 newObserving 數組中n result = f.call(context)n } catch (e) {n result = new CaughtException(e)n }n globalState.trackingDerivation = prevTrackingnn // 比較新舊依賴,更新依賴n bindDependencies(derivation)n return resultn}n
注意響應函數調用時不僅僅是執行了該函數,還觸發了所有被觀察的 Observable 的 reportObserved 方法,從而更新了當前 Derivation 的 newObserving 數組。這部分內容可以回顧第二篇。
再來看看 bindDependencies 方法如何更新依賴:
function bindDependencies(derivation: IDerivation) {n const prevObserving = derivation.observingn const observing = (derivation.observing = derivation.newObserving!)n // 記錄更新依賴過程中,新觀察的 Derivation 的最新狀態n let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATEnn // 遍歷新的 observing 數組,使用 diffValue 這個屬性來輔助 diff 過程:n // 所有 Observable 的 diffValue 初值都是0(要麼剛被創建,繼承自 BaseAtom 的初值0;n // 要麼經過上次的 bindDependencies 後,置為了0)n // 如果 diffValue 為0,保留該 Observable,並將 diffValue 置為1n // 如果 diffValue 為1,說明是重複的依賴,無視掉n let i0 = 0,n l = derivation.unboundDepsCountn for (let i = 0; i < l; i++) {n const dep = observing[i]n if (dep.diffValue === 0) {n dep.diffValue = 1n // i0 不等於 i,即前面有重複的 dep 被無視,依次往前移覆蓋n if (i0 !== i) observing[i0] = depn i0++n }nn // 更新 lowestNewObservingDerivationState n if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {n lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesStaten }n }n observing.length = i0nn derivation.newObserving = null n // 遍歷 prevObserving 數組,檢查 diffValue:(經過上一次的 bindDependencies後,該數組中不會有重複)n // 如果為0,說明沒有在 newObserving 中出現,調用 removeObserver 將 dep 和 derivation 間的聯繫移除n // 如果為1,依然被觀察,將 diffValue 置為0(在下面的循環有用處)n l = prevObserving.lengthn while (l--) {n const dep = prevObserving[l]n if (dep.diffValue === 0) {n removeObserver(dep, derivation)n }n dep.diffValue = 0n }nn // 再次遍歷新的 observing 數組,檢查 diffValuen // 如果為0,說明是在上面的循環中置為了0,即是本來就被觀察的依賴,什麼都不做n // 如果為1,說明是新增的依賴,調用 addObserver 新增依賴,並將 diffValue 置為0,為下一次 bindDependencies 做準備n while (i0--) {n const dep = observing[i0]n if (dep.diffValue === 1) {n dep.diffValue = 0n addObserver(dep, derivation)n }n }nn // 某些新觀察的 Derivation 可能在依賴更新過程中過期n // 避免這些 Derivation 沒有機會傳播過期的信息(#916)n if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {n derivation.dependenciesState = lowestNewObservingDerivationStaten derivation.onBecomeStale()n }n}n
樸素演算法比較新舊 observing 數組的時間複雜度為 O(n^2),這裡藉助 diffValue 屬性的輔助將複雜度降到了 O(n)。
這樣 Reaction 的執行完成,其依賴也得到了更新。當依賴發生變化後,Reaction 會被加入 pendingReactions 中,並重複上述過程。
推薦閱讀:
※花整個大學的時間研究前端好嗎?
※Vue.js 和 MVVM 的小細節
※react-router頁面滾動時,頁面位置問題?
※前端狀態管理請三思
※JS中的閉包為何會產生"副作用,即閉包只能取得包含函數中任何變數的最後一個值"?