- 原文地址:css-animations-with-finite-state-machines
- 原文做者:David Khourshid
- 譯文出自:阿里雲翻譯小組
- 譯文連接:github.com/dawn-teams/…
- 譯者:也樹
- 校對者:靈沼,照天
隨着用戶界面中可能出現的不一樣狀態和狀態間轉換的數目的不斷增加,樣式和動畫的管理很快就變得複雜起來。即便是一個簡單的登陸表單也能夠有不少不一樣的「用戶狀態流」,而且有許多邊界狀況須要考慮。css
狀態機做爲一種很好的編程範式,經過符合直覺和聲明式的方式來管理用戶界面狀態間的過渡。咱們已經在 the Keyframers 中做爲一種簡化複雜動畫和用戶交互流的方式大量使用到了狀態機。react
因此,什麼是狀態機呢?聽起來是很技術向的一個名詞,對嗎?它實際上可能比你想的要更簡單和直觀。(不要直接看 Wikipedia 的介紹,相信我)git
讓咱們從動畫的角度來探索一下狀態機。假設你在編寫一個 loading 動畫,在任意給定時間,它只能處於如下四個狀態之一。github
這很容易理解,你的動畫不可能既處於 loading 狀態又處於 success 狀態中。可是,這些狀態如何在彼此之間過渡是須要重點考慮的。編程
每一個箭頭告訴咱們一個狀態是如何經過事件過渡到另外一個狀態的,而且有些狀態是不可能互相轉換的。(好比說你不可能從 success 狀態到 failure 狀態)。每個箭頭表明一個能夠落地的動畫,或者能夠說是一個過渡。CSS 過渡是用來描述一個視覺狀態在 CSS 中是如何轉換至另外一個視覺狀態的。echarts
換句話說,只要你在使用 CSS 過渡動畫,你就已經在使用狀態機的思想,但你可能沒有意識到這一點。在不一樣狀態間切換時你可能會使用添加或者移除類名的方式在實現:ide
.button {
/* ... button styles ... */
transition: all 0.3s ease-in-out;
}
.button.is-loading {
opacity: 0.5;
}
.button.is-loaded {
opacity: 1;
background-color: green;
}
複製代碼
這樣能夠正常工做,可是你必須確保 is-loading
類名被移除而且 is-loaded
類名被添加,由於更有可能出現的狀況是類名變成 .button.is-loading.is-loaded
。這樣可能會致使不符合預期的反作用。函數
一個更好的方式是使用 data- 屬性。它們只能展現一個值所以在這種場景下是有用的。當你的用戶界面的某部分同時只能在一個狀態下時(好比 loading
或 success
或 error
),更新 data-
屬性是更直接的:工具
const elButton = document.querySelector('.button');
// set to loading
elButton.dataset.state = 'loading';
// set to success
elButton.dataset.state = 'success';
複製代碼
這種方式天然地限制在任意給定的時機裏你的按鈕只存在單個狀態。你可使用 data-state
屬性來表示不一樣的按鈕狀態:
.button[data-state="loading"] {
opacity: 0.5;
}
.button[data-state="success"] {
opacity: 1;
background-color: green;
}
複製代碼
一般來講,有限狀態機由五部分組成:
它還有一些規範:
如今,讓咱們看看咱們如何在 HTML 和 CSS 中表示有限狀態機。
有時,你須要根據當前應用(或某個父組件)的狀態來決定其它組件的樣式。只讀的 data-
屬性一樣也能夠在這種場景下使用,好比:data-show
:
.button[data-state="loading"] .text[data-show="loading"] {
display: inline-block;
}
.button[data-state="loading"] .text[data-show]:not([data-show="loading"]) {
display: none;
}
複製代碼
這是一種用來標記特定的 UI 元素僅僅應該在特定狀態下展現的方式。而後再分別地在須要展現的元素上添加 data-show="..."
便可。若是你的組件在多個狀態下都想顯示,你能夠像下面這樣使用 空格分割屬性選擇器。
<button class="button" data-state="idle">
<!-- 處於 idle 和 loading 狀態時展現下載圖標 --> <span class="icon" data-show="idle loading"></span> <span class="text" data-show="idle">Download</span> <span class="text" data-show="loading">Downloading...</span> <span class="text" data-show="success">Done!</span> </button>
複製代碼
這是對應的 CSS:
/* ... */
.button[data-state="loading"] [data-show~="loading"] {
display: inline-block;
}
複製代碼
data-state
屬性可使用 JavaScript 進行改變:
const elButton = document.querySelector('.button');
function setButtonState(state) {
// set the data-state attribute on the button
elButton.dataset.state = state;
}
setButtonState('loading');
// the button's data-state attribute is now "loading"
複製代碼
隨着應用的逐漸迭代,將全部的 data-
屬性規則添加進來會讓樣式表不斷膨脹而且難以維護,由於你在 JavaScript 文件和樣式表中都須要維護這些不一樣的狀態。同時由於每一個類名和 data-
屬性添加了不一樣的權重,也會讓權重變得異常複雜。爲了減小這些問題帶來的影響,咱們能夠依照如下兩條原則使用動態的 data-active
屬性:
data-show="..."
屬性時,元素應當具備 data-active
屬性。data-hide="..."
屬性時,元素也應當具備 data-active
屬性。下面是在 JavaScrit 實際應用的例子:
const elButton = document.querySelector('.button');
function setButtonState(state) {
// change data-state attribute
elButton.dataset.state = state;
// remove any active data-attributes
document.querySelectorAll(`[data-active]`).forEach(el => {
delete el.dataset.active;
});
// add active data-attributes to proper elements
document.querySelectorAll(`[data-show~="${state}"], [data-hide]:not([data-hide~="${state}"])`)
.forEach(el => {
el.dataset.active = true;
});
}
// set button state to 'loading'
setButtonState('loading');
複製代碼
如今,咱們上面的展現隱藏的樣式能夠被簡化:
.text[data-active] {
display: inline-block;
}
.text:not([data-active]) {
display: none;
}
複製代碼
目前爲止,一切都好。可是咱們想防止改變狀態的函數包含業務邏輯,咱們能夠建立一個狀態機轉換函數,包含當前狀態和觸發事件後轉換到的下個狀態和返回此狀態的邏輯。經過使用 switch 代碼塊,可能像下面這樣:
// ...
function transitionButton(currentState, event) {
switch (currentState) {
case 'idle':
switch (event) {
case 'FETCH':
return 'loading';
default:
return currentState;
}
case 'loading':
switch (event) {
case 'ERROR':
return 'failure';
case 'RESOLVE':
return 'success';
default:
return currentState;
}
case 'failure':
switch (event) {
case 'RETRY':
return 'loading';
default:
return currentState;
}
case 'success':
default:
return currentState;
}
}
let currentState = 'idle';
function send(event) {
currentState = transitionButton(currentState, event);
// change data-attributes
setButtonState(currentState);
}
send('FETCH');
// => button state is now 'loading'
複製代碼
Switch 代碼塊基於事件對狀態之間的轉換進行編碼,咱們可使用對象來簡化它:
// ...
const buttonMachine = {
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'loading'
}
},
loading: {
on: {
ERROR: 'failure',
RESOLVE: 'success'
}
},
failure: {
on: {
RETRY: 'loading'
}
},
success: {}
}
};
let currentState = buttonMachine.initial;
function transitionButton(currentState, event) {
return buttonMachine
.states[currentState]
.on[event] || currentState; // fallback to current state
}
// ...
// use the same send() function
複製代碼
不只這種方式看起來比 Switch 代碼塊更乾淨,同時也是能夠 JSON 序列化的。同時咱們能夠聲明式地對狀態和事件進行枚舉。這就可讓咱們將 buttonMachine
的代碼複製粘貼至可視化工具中,好比xviz:
狀態機的模式讓應用中狀態的處理更簡便,而且讓 CSS 中的樣式過渡更簡潔。總結一下,咱們介紹瞭如下的 data-
屬性:
data-state
表示組件上有限的狀態(如 data-state="loading"
)data-show
決定了當其中一種狀態匹配到 data-state
中的狀態時元素須要增長 data-active
屬性。(如 data-state="idle loading"
)data-hide
決定了當其中一種狀態匹配到 data-state
中的狀態時元素須要移除 data-active
屬性。(如 data-state="success error"
)data-active
在當前元素 data-show
和 data-hide
屬性匹配到 data-state
中的狀態時,動態添加至以上元素。還有如下的編程範式,使用如下屬性,經過 JavaScript 對象定義一個狀態機:
initial
- 狀態機的初始狀態(如 idle
)states
- 一個包含過渡方式和狀態的 Mapon
- 標識了轉換至下個狀態的事件(如 FETCH: "loading"
)transition(currentState, event)
函數,根據當前狀態在狀態機中查找下一個狀態send(event)
函數,包含如下特色:
transition(...)
方法來決定下一個狀態data-
屬性)咱們一樣能夠經過調用 setButtonState(...)
人工測試想要的狀態,這樣就能夠設置合適的 data-
屬性和在特定狀態下幫助咱們開發和 debug 組件。這樣能夠減小爲了到達合適的狀態而不得不進行的一整套繁瑣的流程。
若是你想更深地探究狀態機(和它延伸出來的概念,「狀態表」),能夠查閱下面的資源:
xstate 是一個可以幫助更好地建立和使用狀態機和狀態圖的庫,支持嵌套/扁平的狀態,行爲等等。經過閱讀這篇文章,你已經知道如何去使用它了:
import { Machine } from 'xstate';
const buttonMachine = Machine({
// the same buttonMachine object from earlier
});
let currentState = buttonMachine.initialState;
// => 'idle'
function send(event) {
currentState = buttonMachine.transition(currentState, event);
// change data-attributes
setButtonState(currentState);
}
send('FETCH');
// => button state is now 'loading'
複製代碼
The World of Statecharts 是由 Erik Mogensen 整理的很是棒的資源,能夠透徹地解釋狀態表和如何在用戶界面上應用。 Spectrum Statecharts community 有許多熱心而且樂於助人,同時對 狀態機和狀態表頗有興趣的開發者。 Learn State Machines 是一個經過構建 Instagram 的應用示例來教你學習狀態表基礎概念的課程。 React-Automata 是 Michele Bertoli 開發的使用 xstate 的庫,它可以讓你在 React 中使用狀態表,有不少好處,好比自動生成測試快照。 若是你想了解更多前端用戶界面中狀態機的好處,能夠查看我曾經在 Shop Talk Show 和 Jon Bellah 對 狀態機 的討論。