這次, 將帶著大家解鎖一條新的系列文章:「 有限狀態機與狀態圖」
?什么?又出了一個狀態管理庫?
有些讀者看到這可能頓感 PTSD ,哀嚎"學不動了"~
先別急,待我細細道來。
狀態管理真令人頭疼
當我們寫應用時,其實都是在利用 API 來控制應用的表現。好的 API 一般有三個特性:
那么,大部分人寫的 API 呢?大部分人寫的 API 也有三個特性
當用戶使用我們的應用時,并不會總按我們預想的方式去使用。讓我們假設有一個理想中才存在的用戶,他確實會按照我們理想的方式去使用應用。
拿網絡請求舉例,在這個例子中,我們將發送一個網絡請求,并將請求的結果展示在應用中。
onSearch(query) {
fetch(BD_API + '&tags=' + query)
.then(
data => {
this.setState({ data });
}
);
}
復制代碼
這段代碼看起來很簡單,很容易就完成了。
接下來,讓我們假設后端存在性能問題,或者需要進行一些耗時的運算,這個搜索 API 可能幾秒甚至十幾秒才能返回結果,那么我們需要加一個 的狀態。
onSearch(query) {
this.setState({ loading: true });
fetch(BD_API + '&tags=' + query)
.then(
data => {
this.setState({ data, loading: false });
}
);
}
復制代碼
就像這樣。我們需要在獲取到數據前展示 界面,獲取到數據后,將 設為 false,隱藏 界面,并展示獲取到的結果。
那么我們現在完成了么?并沒有。如果請求中出錯了呢?我們必須隱藏 界面,展示錯誤提示。
onSearch(query) {
this.setState({ loading: true });
fetch(BD_API + '&tags=' + query)
.then(
data => {
this.setState({ data, loading: false });
}
).catch(error => {
this.setState({
loading: false,
error: true
});
});
}
復制代碼
現在我們是不是 bug free 了?
并沒有。我們還需要確保用戶再次發起請求時,清空了錯誤狀態。
onSearch(query) {
this.setState({
loading: true,
error: false
});
fetch(BD_API + '&tags=' + query)
.then(
data => {
this.setState({
data,
loading: false,
error: false
});
}
).catch(error => {
this.setState({
loading: false,
error: true
});
});
}
復制代碼
正如你所看到的酒店管理系統狀態機圖,應用的復雜性越來越大,我們的心智負擔也在不斷加大。我相信這時候你們能夠聯想到一些實際場景。
那么,如果這時候 PM 又加需求了,我們現在需要提供取消請求的能力了?
如同之前的假設,這個請求耗時太長了,用戶可能會發起另外一個請求來取代這一個請求。
onSearch(query) {
if (this.state.loading) {
return;
}
this.setState({
loading: true,
error: false,
canceled: false
});
fetch(BD_API + '&tags=' + query)
.then(
data => {
if (this.state.canceled) {
return;
}
this.setState({
data,
loading: false,
error: false
});
}
).catch(error => {
if (this.state.canceled) {
return;
}
this.setState({
loading: false,
error: true
});
});
}
onCancel() {
this.setState({
loading: false,
error: false,
canceled: true
});
}
復制代碼
這時我們的代碼將變得更加復雜,在這樣一個小小的搜索事件中,我們處理了非常多的邏輯。
大家或許聽說過 (意大利面) Code,也就是邏輯之間互相耦合依賴,非常難以維護的代碼。
或許有些人會說:“我不寫 Code,我的代碼都是模塊化、分層設計、高內聚低耦合的”。
即便如此,絕大多數的人都會陷入另一個陷阱: (千層面) Code,即在代碼片段之外,代碼模塊之間也互相耦合。
為了解決這一問題,我們可以通過一種自底向上的模式處理邏輯。
在這種模式下,無論是處理 還是 事件,所有的邏輯都是在 event (事件) 之下。
每個 event 可能對應許多不同的 (行為),并且其中一些 還會修改 state (狀態)
但是必須小心地為 event 選擇合適的 ,否則很可能用錯誤的方式修改 state,比如在 內寫了一連串的 if else 或者 語句。
這樣的代碼很快就會變得難以維護。因為所有的邏輯只存在于你的腦袋里,當你寫測試時,必須從記憶深處找回并解讀出來。
拿剛才的示例代碼舉例,如果你嘗試對新加入的團隊成員講解,你會發現讓他們理解這段邏輯并不容易,更別說一整個項目了。
這也會使代碼更難擴展,就像我們剛才引入取消功能時,加入難度遠比之前的功能點要大。而新加入的功能,比如“取消請求”,會成倍地使代碼變得更難維護。
讓我們從另一個角度繼續思考。
當我們需要實現一組互相有依賴的組件。我們會用分離組件的框架,比如 React,去實現這些組件。這些組件能夠直接被嵌入頁面中的任何位置。
在設計上,它們邏輯間互相分離,通過 props 建立關系。但是在實際場景中,不同組件間并不是無關的。我們需要組織好組件間的嵌套、創建、修改和通信。
那么,我們的解決方案是什么呢?
解決方案: 有限狀態機與狀態圖
許多人在學校可能有學習過狀態機的相關概念和學術定義,看學術定義或許理解成本比較高,讓我們來通過例子直觀理解下。
有限狀態機包含五個重要部分
舉個例子,當我們 fetch 時會返回一個 ,這時它進入 狀態。如果它被 ,進入 狀態。如果它被 ,進入 狀態。
對于應用開發來說,大部分狀態都是連續的。相對而言,最終狀態出現的比例會小很多,在 中, 和 就是它的最終狀態。
基于有限狀態機實現搜索
回到前面的搜索問題,我們可以用有限狀態機對其建模。
默認狀態為 idle酒店管理系統狀態機圖,當我們觸發了 事件,應用會進入 狀態。
如果我們在 狀態下,再觸發 事件,應用仍處于 狀態。
接下來,我們可以 或者 搜索的結果,并分別進入 或 狀態。
在這兩種狀態下,我們可以再次發起新的 事件,通過箭頭指向,我們可以清晰地看出它將回到 狀態。
上面的狀態機邏輯可以寫成一個 JSON 對象(比起黑盒函數,JSON 或許更加可讀,它能用簡單的方式枚舉所有可能的 , 以及 )
const machine = {
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' },
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
};
function transition(state, event) {
return machine.states[state].on[event];
}
復制代碼
完整代碼見:.io/s/-se…
基于有限狀態機實現 Live Share
舉一個 作者本人的例子, 的作者 David 來自微軟,他還開發了 的 Live Share 插件,可以用來結對編程、面試或者代碼分享。
David 在開發這個插件時,因為復雜的邏輯,寫了很多 bug。尤其是這類工具類應用,我們需要在同一個頁面停留,不斷處理非常多的狀態。
拿登錄舉例。登錄后進入 In 狀態,這時可以做兩件事,share (分享) 一個 (會話) 或者 join (加入) 一個 。登錄失敗,需要返回 Out 狀態。
以上是基本流程。除此之外,用戶可能在 share 的過程中 sign out,還可能在 share 的同時嘗試 join 另一個 。這些邏輯可以歸類為一堆 if else,但利用狀態機可以使它一目了然。
并且,通過監聽狀態轉移,作者輕松實現了 Live Share 后來新增的埋點需求。
transition(currentState, event) {
const nextState = //...
Telemetry.sendEvent(
currentState,
nextState,
event
);
return nextState;
}
復制代碼
從圖上能夠明顯看出,用戶進行 sign in 的頻率最高。
登錄后,用戶進行 share 和 join 的頻率差不多。同時,它也清晰地展示了有多少用戶進入了 狀態,多少用戶進入了 error 狀態。
是對有限狀態機( state )和狀態圖()面向現代 Web 開發的 js 實現, 沒有自己創造新的概念,而是遵循 W3C 對 SCXML (State Chart ) 進行了實現。
目前為止,在 上已經有 17k 的 Star 數。
有良好的生態支持,包括
等等
后續,我們將繼續分享如何利用可視化工具,降低開發中的心智負擔,提升開發效率。
官方文檔:/docs/…
迫不及待想要直接上手的朋友可以看看官方的 Todo MVC 樣例:/docs/…
PS:團隊正在招聘中!