MobX流程分析與最佳實踐
大力輔導(dǎo)項目在首頁多個 tab,課程詳情、社區(qū)、語文字詞和個人信息頁等多個業(yè)務(wù)場景深度使用 Flutter 進行開發(fā),而在 Flutter 開發(fā)過程中狀態(tài)管理是繞不開的話題。
在進行 Native 開發(fā)時,我們命令式地來表述 UI 構(gòu)建和更新邏輯,通過類似 setText、setImageUrl 的代碼對界面 UI 進行構(gòu)建與更新。和 Native 開發(fā)不同,在進行 Flutter 開發(fā)時,UI 的構(gòu)建是聲明式的,這種框架結(jié)構(gòu)直接影響了我們對更新邏輯的表達形式。
Flutter 中觸發(fā)狀態(tài)更新的 API 即我們最熟悉的 setState 方法,但是項目中往往會碰到狀態(tài)需要跨層級或者在兄弟組件之間共享,僅僅使用 setState 一般不足以覆蓋復(fù)雜狀態(tài)管理的場景。因此我們需要狀態(tài)管理框架來幫助我們規(guī)范更新邏輯,同時也能更好地貼合 Flutter framework 的工作機制。
框架選型
我們在初期調(diào)研了多個開源的狀態(tài)管理框架包括 BLoC、Redux 以及 MobX 等。
BLoC 使用流共享數(shù)據(jù),并且 Dart 語言本身對流的親和度很高,參考其它平臺的 ReactiveX 的解決方案,開發(fā)者可以快速地使用這種模式進行開發(fā)。BLoC 的最大問題是,其它的 ReactiveX 每個數(shù)據(jù)源都是獨立的 Stream,但是 BLoC 則是統(tǒng)一的單 Stream。單 Stream 表達整個頁面的所有業(yè)務(wù)邏輯不具有普適性,其抽象層級過高,部分場景需要配合其他的方案。
就 Js 領(lǐng)域最流行的 Redux 框架而言,由于 Redux 本身的一些特點,Redux 的主打功能是應(yīng)用狀態(tài)可預(yù)知、可回溯,同時它也有使用上的成本,比如要求 reducer 是純函數(shù),store 之間的交流需要最佳實踐指導(dǎo),樣板代碼較多,可能需要開發(fā)同學(xué)有一定的 FP 開發(fā)背景。Redux 是諸多框架中編碼最為繁瑣,樣板代碼較多的一個。不過大量的模板代碼也規(guī)范了代碼風(fēng)格,大型項目中,Redux 更規(guī)范易操作擴展和維護。
MobX 的數(shù)據(jù)響應(yīng)對開發(fā)者幾乎完全透明,開發(fā)者可以更加自由地組織自己的應(yīng)用程序的狀態(tài),擁有比較高的易用性和擴展性,也易于學(xué)習(xí),更加符合 OOP 的思想,也可以更快地支持業(yè)務(wù)迭代。使用 MobX,使得我們更加可以關(guān)注狀態(tài)本身和狀態(tài)引起的變化,不需要關(guān)心那么多復(fù)雜組件是如何組合連接起來的,所有的事情都被簡單優(yōu)雅的 API 抽象掉了。不過,MobX 自身過于自由的特性也帶來了一些麻煩,由于編碼沒有模板約束,過于自由,容易導(dǎo)致團隊代碼風(fēng)格不統(tǒng)一,不同的頁面不同的風(fēng)格,代碼難以管理維護,對此往往需要制定統(tǒng)一的團隊編碼規(guī)范。
基于我們項目目前的規(guī)模,以及迭代速度,同一個頁面相鄰版本的兩次迭代很有可能發(fā)生了很大的變化,MobX 的簡單易用性最有利于我們的項目進行高強度快速迭代。最終我們在諸多框架中,選擇了使用 MobX 作為我們項目的狀態(tài)管理框架,本文著重分析 MobX 數(shù)據(jù)綁定和更新的主流程,以及最佳實踐。
使用方法不再詳述,參見 MobX.dart 官網(wǎng),我們著重分析一下 MobX 驅(qū)動頁面更新的主流程,包含兩部分:數(shù)據(jù)綁定與數(shù)據(jù)更新。分析的代碼基于 MobX 的 1.1.0 版本。
為了更直觀的分析,我們直接使用官網(wǎng)經(jīng)典的 MobX Counter 這個 demo 進行示例,通過 debug 的堆棧幫助我們?nèi)ヌ骄?MobX 中數(shù)據(jù)的綁定和更新的主流程。

數(shù)據(jù)綁定流程
Observer 和 @observable 對象一定通過某種方式建立了綁定關(guān)系,我們先來研究一下數(shù)據(jù)的綁定流程。
從 reportRead() 入手
Atom.reportRead()
在代碼中獲取顯示的數(shù)字 counter.value 處打一個斷點,從 demo app 打開開始,第一次頁面 build 時,代碼會執(zhí)行到生成的. g.dart 中去。我們來看 value 相關(guān)的 get 方法和 Atom 對象:
final _$valueAtom = Atom(name: '_Counter.value');
@override
int get value {
_$valueAtom.reportRead();
return super.value;
}
復(fù)制代碼
生成的. g.dart 文件中有一個 Atom 對象,其中覆寫了 counter.value 的 get 方法,我們每次使用 @observable 標(biāo)記一個字段,在 .g.dart 中就會生成該字段的 getter 跟 setter 及對應(yīng)的 Atom 對象。Atom 對象是對原字段的一個封裝,當(dāng)我們讀取 couter.value 字段時,會在該 Atom 上調(diào)用 reportRead():
extension AtomSpyReporter on Atom {
void reportRead() {
...
reportObserved();
}
...
}
void reportObserved() {
_context._reportObserved(this);
}
復(fù)制代碼
ReactiveContext._reportObserved()
這個_context,追溯一下可以看到是一個全局的 ReactiveContext 單例,注釋寫的比較明白了:

它負責(zé)處理 Atom 跟 Reaction(下文會講到) 的依賴關(guān)系, 及進行數(shù)據(jù)方法綁定、分發(fā)、解綁等邏輯。
最終走到了 context 中的_reportObserved 方法,這個 Atom 對象被添加到了一個 derivation 的_newObservables 字段中,該_newObservables 類型為 Set:
void _reportObserved(Atom atom) {
final derivation = _state.trackingDerivation;
if (derivation != null) {
derivation._newObservables.add(atom);
if (!atom._isBeingObserved) {
atom
.._isBeingObserved = true
.._notifyOnBecomeObserved();
}
}
}
復(fù)制代碼
Atom 對象被類型為 Derivation 的變量 derivation 持有在一個_newObservables 的 Set 里面,我們回到之前打斷點的堆棧,來看一下這里的 derivation 到底是什么。
回到起點
堆棧信息
下圖為整個頁面從 main.dart 的 runApp 開始到 MyHomePage 這個 Widget 的 build 的過程,從 debug 的堆棧信息入手:

Observer 相關(guān)的 Widget 和 Element
首先我們簡單看一下 Observer 以及相關(guān)的 Widget 和 Element 的概念。我們通常使用的 Observer 這個 Widget,它實際上是一個 StatelessObserverWidget(繼承自 StatelessWidget),其 build 方法中的 Widget 就是 builder 中返回的 widget,該 StatelessObserverWidget 還 mixin 了 ObserverWidgetMixin,StatelessObserverWidget 的 Element 為 StatelessObserverElement,該 Element 也 mixin 了 ObserverElementMixin,他們之間的關(guān)系如圖所示:

ObserverElementMixin.mount()
從該部分開始看起:
@override
void mount(Element parent, dynamic newSlot) {
_reaction = _widget.createReaction(invalidate, onError: (e, _) {
... ));
}) as ReactionImpl;
...
}
復(fù)制代碼
ObserverElementMixin 的 mount 方法給_reaction 賦了值,再追溯一下,ObserverWidgetMixin 的 createReaction 方法傳入了上文提到的核心的 ReactiveContext 單例,創(chuàng)建了 Reaction,而 Reaction 實現(xiàn)了 ReactionImpl 類,ReactionImpl 又實現(xiàn)自 Derivation,在 Derivation 類中我們看到了上一部分提到的_newobservables 這個 Set:

此時可以有一個初步的猜想:由 Observer 這個 Widget 持有了 ReactionImpl,ReactionImpl 中持有了_newobservables 這個 Set,在 @observable 變量被讀取的時候通過對應(yīng) Atom 對象的 reportRead 方法將該 Atom 對象添加入了這個 Set,這樣就 Observer 這個 Widget 通過其中的 ReactionImpl 間接的持有了 @observable 對象。
繼續(xù)往下看我們來驗證一下。
ObserverElementMixin.build()
按著堆棧信息走下去。來到 ObserverElementMixin 的 build() 方法,調(diào)用了 mount 中創(chuàng)建的 ReactionImpl 的 track() 方法:
Widget build() {
...
reaction.track(() {
built = super.build();
});
...
}
復(fù)制代碼
ReactionImpl.track() -> ReactiveContext.trackDerivation()
此處剔除掉了大部分和主流程無關(guān)的代碼,如下:
void track(void Function() fn) {
...
_context.trackDerivation(this, fn);
...
}
//主流程關(guān)注這兩句
T trackDerivation<T>(Derivation d, T Function() fn) {
final prevDerivation = _startTracking(d);
...
result = fn();
...
_endTracking(d, prevDerivation);
...
}
復(fù)制代碼
ReactiveContext 的 trackDerivation() 方法接收 Derivation 參數(shù),這里傳入自身,來到下面,在_startTracking 和_endTracking 之間調(diào)了 fn,這里的 fn 就是 ObserverElementMixin 的 build 方法中傳入的 super.build():
ReactiveContext._startTracking()
_startTracking() 中做的是對狀態(tài)的更新。其中的_state 是個_ReactiveState,就是一個對 ReactiveContext 單例當(dāng)前狀態(tài)的封裝的類,這里我們關(guān)注 trackingDerivation,是當(dāng)前正在被記錄的一個 Derivation。
_startTracking() 中最重要的一步是把_state 中記錄的 trackingDerivation 賦值為當(dāng)前的 Derivation(即上方傳入的 ReactionImpl),這一步很關(guān)鍵,直到_endTracking 執(zhí)行之前,這個 state.trackingDerivation 都是當(dāng)前設(shè)置的值,并返回一個 prevDerivation(上一個記錄的 trackingDerivation):
Derivation _startTracking(Derivation derivation) {
final prevDerivation = _state.trackingDerivation;
_state.trackingDerivation = derivation;
_resetDerivationState(derivation);
derivation._newObservables = {};
return prevDerivation;
}
復(fù)制代碼
Observer.builder 調(diào)用的位置
在_startTracking 和_endTracking 之間調(diào)用了 fn,即 ObserverElementMixin 中傳入的 super.build(),熟悉 dart mixin 語法規(guī)則的,也很快清楚這里調(diào)用鏈最終會走到 Observer 這個 Widget 的 build 方法,也即我們使用時傳入的 builder 方法里面去:
class Observer extends StatelessObserverWidget implements Builder {
...
@override
Widget build(BuildContext context) => builder(context);
...
}
復(fù)制代碼
這時候就回到了一開頭的部分,builder 中讀取了 counter.value,也即調(diào)用它的 get 方法,通過 reportRead,最終再通過 state.trackingDerivation 得到當(dāng)前正在記錄的 derivation 對象,并給他的_newObservables 的這個 Set 里面添加了 counter.value 對應(yīng)的封裝的 Atom 對象。
解決一開始我們提出的問題——持有_newObservables 的 derivation 是什么?
derivation 就是_startTracking() 方法中賦值給_state.trackingDerivation 的當(dāng)前 ObserverElementMixin 中持有的 ReactionImpl 對象。Observer 通過該對象間接持有了我們的 @observable 對象,也驗證了我們上文的猜想。
回顧下,經(jīng)過上面_startTracking 中將當(dāng)前的 derivation 賦值給 context.state.trackingDerivation ,以及 Observer 的 builder 方法(fn)的調(diào)用,builder 方法中任何對 @observable 對象的 get 方法,都將經(jīng)過 reportRead,也就是 reportObserved,所以該 @observable 對象就會被添加到當(dāng)前的 derivation 的 _newObservables 集合上,表示該 derivation 和 @observable 對象的依賴關(guān)系,注意此時這樣的綁定關(guān)系是單向的,目的是為了收集依賴。真正的數(shù)據(jù)綁定過程在_endTracking() 中。
ReactiveContext._endTracking()
最后再看_endTracking,核心的建立綁定關(guān)系的方法是_bindDependencies:
void _endTracking(Derivation currentDerivation, Derivation prevDerivation) {
_state.trackingDerivation = prevDerivation;//這里又會把trackingDerivation恢復(fù)回去
_bindDependencies(currentDerivation);
}
void _bindDependencies(Derivation derivation) {
//derivation里面實際上有兩個set _observables和_newObservables,分別裝的是之前舊的atom和reportRead里面新加的atom
//搞了兩次difference, 把新的和舊的@observable變量分開。舊的清空數(shù)據(jù),新的綁定觀察者
final staleObservables =
derivation._observables.difference(derivation._newObservables);
final newObservables =
derivation._newObservables.difference(derivation._observables);
var lowestNewDerivationState = DerivationState.upToDate;
// Add newly found observables
for (final observable in newObservables) {
observable._addObserver(derivation);//綁定觀察者
// Computed = Observable + Derivation
if (observable is Computed) {
if (observable._dependenciesState.index >
lowestNewDerivationState.index) {
lowestNewDerivationState = observable._dependenciesState;
}
}
}
// Remove previous observables
for (final ob in staleObservables) {
ob._removeObserver(derivation);//解除綁定
}
if (lowestNewDerivationState != DerivationState.upToDate) {
derivation
.._dependenciesState = lowestNewDerivationState
.._onBecomeStale();
}
derivation
.._observables = derivation._newObservables
.._newObservables = {}; // No need for newObservables beyond this point
}
//下面是atom的_addObserver和_removeObserver方法
//atom中有個observers變量 Set<Derivation>對象,記錄了觀察自己的Derivation。
void _addObserver(Derivation d) {
_observers.add(d);
if (_lowestObserverState.index > d._dependenciesState.index) {
_lowestObserverState = d._dependenciesState;
}
}
void _removeObserver(Derivation d) {
_observers.remove(d);
if (_observers.isEmpty) {
_context._enqueueForUnobservation(this);
}
}
復(fù)制代碼
這個方法的邏輯,根據(jù)前后兩次 build 時 Set 中收集 Atom 對象的依賴,分別執(zhí)行 _addObserver 和 _removeObserver,這樣,每個 @observable 對象上的 observers 集合都會是最新的了。
結(jié)論
Observer 對應(yīng)的 Element——StatelessObserverElement,持有一個 Derivation——即 ReactionImpl 對象 reacton**,** 而該對象持有一個 Set 類型的_observables,@observable 對象在被讀取調(diào)用 get 方法的時候,對應(yīng)的 Atom 被添加到了這個 Set 中去,該 Set 中的 @observable 對象對應(yīng)的 Atom 在 endTracking 中調(diào)用了_addObserver 方法,把觀察自己的 ReactionImpl 添加進 observers 這個 Set 中去。從而 @obsevable 對象對應(yīng)的 Atom 持有了 Observer 這個 Widget 中的 ReactionImpl,Observer 就這樣和 @observable 對象建立了綁定關(guān)系。
數(shù)據(jù)更新流程
知道了 Observer 和 @observable 對象是怎樣建立聯(lián)系之后,再來看一下當(dāng)我們修改 @observable 對象時候,更新界面邏輯是怎么觸發(fā)的。
reportWrite()
在. g.dart 文件中,覆寫了 @observable 變量的 get 方法,會在 get 時候調(diào)用對應(yīng) Atom 對象的 reportRead(),并且這里還覆寫了 @observable 變量的 set 方法,都會調(diào)用 Atom 對象的 reportWrite() 方法,這個方法做了兩件事情
更新數(shù)據(jù)
把與之綁定 的 derivation (即 reaction) 加到更新隊列。
@override set value(int value) { _$valueAtom.reportWrite(value, super.value, () { super.value = value; }); }
最終可以追溯到這里:
void propagateChanged(Atom atom) {
if (atom._lowestObserverState == DerivationState.stale) {
return;
}
atom._lowestObserverState = DerivationState.stale;
_observers就是上面數(shù)據(jù)綁定過程中涉及到的atom對象記錄觀察者的Set<Derivation>
for (final observer in atom._observers) {
if (observer._dependenciesState == DerivationState.upToDate) {
observer._onBecomeStale();
}
observer._dependenciesState = DerivationState.stale;
}
}
復(fù)制代碼
ReactionImpl._onBecomStale()
@override
void _onBecomeStale() {
schedule();
}
void schedule() {
...
_context
..addPendingReaction(this)
..runReactions();
}
復(fù)制代碼
ReactiveContext.addPendingReaction()
reaction 添加到隊列,reaction也就是上面?zhèn)魅氲腞eactionImpl
void addPendingReaction(Reaction reaction) {
_state.pendingReactions.add(reaction);
}
復(fù)制代碼
ReactiveContext.runReactions
void runReactions() {
...
for (final reaction in remainingReactions) {
reaction._run();
}
_state
..pendingReactions = []
..isRunningReactions = false;
}
復(fù)制代碼
ReactionImpl.run()
@override
void _run() {
...
_onInvalidate();//這里實際上就是觸發(fā)更新的地方
...
}
復(fù)制代碼
這邊的_onInvalidate() 就是在 ObserverElementMixin.mount() 里面 createReaction 時候傳進去的
@override
void mount(Element parent, dynamic newSlot) {
_reaction = _widget.createReaction(invalidate, onError: (e, _) {
... ));
}) as ReactionImpl;
...
}
復(fù)制代碼
看看 invalidate 是什么:
void invalidate() => markNeedsBuild();
復(fù)制代碼
也就是 markNeedsBuild 標(biāo)臟操作,這樣 Flutter Framework 的 buildOwner 會在下一幀重新調(diào)用 build 方法,就完成了數(shù)據(jù)更新操作。
結(jié)論
至此數(shù)據(jù)更新的流程也搞明白了,在更改 @observable 變量的時候,調(diào)用到 Atom 對象的 reportWrite 方法,首先更新了數(shù)據(jù),然后把與之綁定的 ReactionImpl 對象 derivation 加到隊列 pendingReactions,最終隊列里面的 ReactionImpl 調(diào)用 run 方法,觸發(fā) markNeedsBuild,完成了界面更新。
在使用 MobX 進行狀態(tài)管理的過程中,我們也踩了一些坑,總結(jié)了最佳實踐,對開發(fā)過程中時常遇到的更改數(shù)據(jù)頁面未被更新的情況做了總結(jié)。
因為 MobX 的數(shù)據(jù)綁定是運行時的,所以需要注意綁定不要寫在控制流語句中,同時也要注意綁定的層級。在此看三個 bad case,同時引出最佳實踐。相信在了解了框架數(shù)據(jù)綁定和更新的原理之后,也很容易理解這些 bad case 出現(xiàn)的原因。
Bad Case 1:
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
if (store.showImage) {
child = Image.network(
store.imageURL
);
} else {
// ...
});
}
復(fù)制代碼
這個例子里面 store.imageURL 是一個被 @observable 標(biāo)注的字段。如果在第一次 build 的過程中,即數(shù)據(jù)綁定的過程中,store.showImage 為 false,代碼走 else 分支,這樣 store.imageURL 就沒能和 Observer 建立綁定關(guān)系,后續(xù) store.imageURL 發(fā)生改變,就無法驅(qū)動界面更新。
Bad Case 2:
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
if (store.a && store.b && store.c) {
child = Image.network(
store.imageURL
);
} else {
// ...
});
}
復(fù)制代碼
這個例子里面 store.a、store.b 還有 store.c 都是 @observable 標(biāo)注的 bool 變量,遵循大部分語言的邏輯表達式判斷規(guī)則,if 語句中多個并列的與的條件,如果排列靠前的條件為 false,那么后續(xù)的條件不會再被判斷,直接走入 else 分支。
那么問題也顯而易見了,如果本意是希望 store.a、store.b 還有 store.c 都和 Observer 綁定關(guān)系,如果在第一次 build 時,store.a 為 false,那么 b 和 c 均沒有和 Observer 建立聯(lián)系,這樣 b 和 c 的變化就無法驅(qū)動該 Widget 更新。
Bad Case 3:
針對我們開發(fā)過程中一個常見的錯誤舉出這個 Case:
class WidgetA extends StatelessWidget{
Widget build(BuildContext context) {
...
Observer(builder:(context){
return TestWidget();
});
...
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Text(
'${counter.value}',
style: Theme.of(context).textTheme.headline4,
),
onTap: () {
counter.increment();
},
);
}
}
復(fù)制代碼
這個例子改編自 MobX 官網(wǎng)經(jīng)典的 counter Demo,counter.value 是 @observable 標(biāo)注的字段。編寫者的本意是用 Observer 包裹了 WidgetB,希望 GestureDetector 的點擊事件使得 counter.value 自增,驅(qū)動 Observer 的 Widget 的更新,不過我們點擊按鈕發(fā)現(xiàn)頁面并沒有更新。
根據(jù)上述原理分析,數(shù)據(jù)綁定的過程是在_startTracking 和_endTracking 之間的 Observer.build 方法的調(diào)用過程中完成的。而這里 Observer.builder 中只是 return 了 TestWidget,也即調(diào)用了 WidgetB 的構(gòu)造方法,WidgetB 的 build 方法,也即讀取 counter.value 的方法是在下一層 widget 構(gòu)建的過程中,才會被調(diào)用,因此 counter.value 未能和它上一層的 Observer 建立綁定關(guān)系,自然也不能夠驅(qū)動頁面更新了。
Good
我們針對 Bad Case 2 提出最佳實踐:
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
bool a = store.a;
bool b= store.b;
bool c = store.c;
if (a && b && c) {
child = Image.network(
store.imageURL
);
} else {
// ...
});
}
復(fù)制代碼
對于 @observable 對象的依賴依次羅列在最開始,而不是寫在 if 判斷括號中,就可以保證所有變量均和 Observer 建立了綁定關(guān)系。
MobX 還有許多其他的細節(jié),比如,context 上的 startBatch 相關(guān),這是因為 action 中可以調(diào)用其他的 action,為了減少不必要的更新通知調(diào)用,通過 batch 機制合并 pendingReaction 的調(diào)用。同理,在 reaction 內(nèi)部也可以對 @observable 對象進行更新,因此也需要 batch 機制合并更改。
MobX 也有一些優(yōu)化點,比如,上述數(shù)據(jù)更新的 reportWrite 方法,我們可以 diff 一下 oldValue 和 value,看二者是否相等,不相等的時候再進行后續(xù)流程。
有興趣的讀者可以自行閱讀源碼探索更多的內(nèi)容,在此不作詳細分析了。

關(guān)注公眾號「前端Sharing」,持續(xù)為你推送精選好文。
