有限状态机在 CSS 动画中的应用
有限状况机在 CSS 动画中的运用
跟着用户界面中或许呈现的不同状况和状况间转化的数目的不断增长,款式和动画的办理很快就变得杂乱起来。即使是一个简略的登录表单也能够有许多不同的“用户状况流”,而且有许多鸿沟状况需求考虑。
示例:https://codepen.io/davidkpiano/pen/WKvPBP
状况机作为一种很好的编程范式,经过符合直觉和声明式的方法来办理用户界面状况间的过渡。咱们现已在 the Keyframers 中作为一种简化杂乱动画和用户交互流的方法大量运用到了状况机。
所以,什么是状况机呢?听起来是很技能向的一个名词,对吗?它实践上或许比你想的要更简略和直观。(不要直接看 Wikipedia 的介绍,相信我)
让咱们从动画的角度来探求一下状况机。假设你在编写一个 loading 动画,在恣意给定时刻,它只能处于以下四个状况之一。
- idle (还未进入 loading 状况)
- loading
- failure
- success
这很容易了解,你的动画不或许既处于 loading 状况又处于 success 状况中。可是,这些状况如安在彼此之间过渡是需求重点考虑的。
每个箭头告诉咱们一个状况是如何经过事件过渡到另一个状况的,而且有些状况是不或许互相转化的。(比方说你不或许从 success 状况到 failure 状况)。每一个箭头代表一个能够落地的动画,或者能够说是一个过渡。CSS 过渡是用来描述一个视觉状况在 CSS 中是如何转化至另一个视觉状况的。
换句话说,只要你在运用 CSS 过渡动画,你就现已在运用状况机的思维,但你或许没有意识到这一点。在不同状况间切换时你或许会运用增加或者移除类名的方法在完成:
.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;
}
有限状况机
通常来说,有限状况机由五部分组成:
- 一系列有限的状况(如 idle,loading,success,failure)
- 一系列有限的事件(如 FETCH,ERROR,RESOLVE,RETRY)
- 一个初始状况(如 idle)
- 一系列过渡方法(如 idle 经过 FETCH 事件过渡至 laoding)
- 终究状况
它还有一些规范:
- 一个有限状况机一起只能在一种状况中
- 一切的过渡方法有必要是确定的,意味着恣意给定的状况和时刻,必定会导致相同的预定义的下一个状况。没有意外。
现在,让咱们看看咱们如安在 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="..." 即可。假如你的组件在多个状况下都想显示,你能够像下面这样运用 空格分割特点选择器。
这是对应的 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- 特点款式
跟着运用的逐渐迭代,将一切的 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 - 一个包括过渡方法和状况的 Map
- on - 标识了转化至下个状况的事件(如 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 对 状况机 的讨论。
我有话说: