• 原文地址:css-animations-with-finite-state-machines
  • 原文作者:David Khourshid
  • 译文出自:阿里云翻译小组
  • 译文链接:github.com/dawn-teams/…
  • 译者:也树
  • 校对者:灵沼,照天

随着用户界面中可能出现的不同状态和状态间转换的数目的不断增长,样式和动画的管理很快就变得复杂起来。即使是一个简单的登录表单也可以有很多不同的“用户状态流”,并且有许多边界情况需要考虑。

示例:codepen.io/davidkpiano…

状态机作为一种很好的编程范式,通过符合直觉和声明式的方式来管理用户界面状态间的过渡。我们已经在 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- 属性。它们只能展示一个值因此在这种场景下是有用的。当你的用户界面的某部分同时只能在一个状态下时(比如 loadingsuccesserror),更新 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="..." 即可。如果你的组件在多个状态下都想显示,你可以像下面这样使用 空格分割属性选择器。

<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 buttonelButton.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 attributeelButton.dataset.state = state;// remove any active data-attributesdocument.querySelectorAll(`[data-active]`).forEach(el => {delete el.dataset.active;});// add active data-attributes to proper elementsdocument.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-attributessetButtonState(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-showdata-hide 属性匹配到 data-state 中的状态时,动态添加至以上元素。

还有以下的编程范式,使用以下属性,通过 JavaScript 对象定义一个状态机:

  • initial - 状态机的初始状态(如 idle
  • states - 一个包含过渡方式和状态的 Map
  • on - 标识了转换至下个状态的事件(如 FETCH: "loading"
  • 创建一个 transition(currentState, event) 函数,根据当前状态在状态机中查找下一个状态
  • 创建一个 send(event) 函数,包含以下特点:
    1. 调用 transition(...) 方法来决定下一个状态
    2. 设置当前状态为获取到的下一个状态
    3. 执行相应的副作用(在这里是设置合适的 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-attributessetButtonState(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 对 状态机 的讨论。

「译」有限状态机在 CSS 动画中的应用相关推荐

  1. 「译」Liftoff:V8 引擎中全新的 WebAssembly baseline 编译器

    翻译自:Liftoff: a new baseline compiler for WebAssembly in V8 Monday, August 20, 2018 V8 引擎在 v6.9 版本中加入 ...

  2. js最小化浏览器_「译」解析、抽象语法树(ast) +如何最小化解析时间的5个技巧...

    前言 该系列课程会在本周陆续更新完毕,主要讲解的都是工作中可能会遇到的真实开发中比较重要的问题以及相应的解决方法.通过本系列的课程学习,希望能对你日常的工作带来些许变化.当然,欢迎大家关注我,我将持续 ...

  3. iOS 9,为前端世界都带来了些什么?「译」

    2015 年 9 月,Apple 重磅发布了全新的 iPhone 6s/6s Plus.iPad Pro 与全新的操作系统 watchOS 2 与 tvOS 9(是的,这货居然是第 9 版),加上已经 ...

  4. jvm 系列(九):如何优化 Java GC 「译」

    本文由CrowHawk翻译,地址:如何优化Java GC「译」,是Java GC调优的经典佳作. Sangmin Lee发表在Cubrid上的"Become a Java GC Expert ...

  5. 变量、中文-「译」javascript 的 12 个怪癖(quirks)-by小雨

    在写这篇文章之前,xxx已经写过了几篇关于改变量.中文-主题的文章,想要懂得的朋友可以去翻一下之前的文章 原文:12 JavaScript quirks 译文:「译」javascript 的 12 个 ...

  6. jvm系列(十):如何优化Java GC「译」

    本文由CrowHawk翻译,地址:如何优化Java GC「译」,是Java GC调优的经典佳作. Sangmin Lee发表在Cubrid上的"Become a Java GC Expert ...

  7. 「译」一起探讨 JavaScript 的对象

    「译」一起探讨 JavaScript 的对象 原文地址:Let's explore objects in JavaScript 原文作者:Cristi Salcescu 译文出自:阿里云翻译小组 译文 ...

  8. android linux 优化,【「Android」UE手游研发中,如何做好Android内存优化?】|Linux|DEX|腾讯游戏|_傻大方...

    傻大方提要:[「Android」UE手游研发中,如何做好Android内存优化?]编者按在大年夜多半人的印象里,用UE引擎制造出来的游戏实际占用内存会比较高.腾讯游戏学院专家Leonn,将和大年夜家分 ...

  9. 「译」一个3D网页是如何制作的

    「译」一个3D网页是如何制作的 原文: 本文作者制作了一个3D网页作为自己的个人主页,是一个遥控汽车的游戏页面.页面十分有趣,感兴趣的朋友可以先打开体验一下. 以下为原文的译文,是我个人理解的版本.大 ...

最新文章

  1. 【Python】pdf2image模块+poppler将PDF转换为图片
  2. 人类长非编码RNA表达数据库,整合9种重要生物学场景(发育、癌症、病毒侵染等)...
  3. OpenShift 4 之 GitOps(5)用ArgoCD配置其他OpenShift资源
  4. oracle中断进程,中断ORACLE数据库关闭进程导致错误案例
  5. php time豪秒_PHP精确到毫秒秒杀倒计时实例详解
  6. python中out什么意思_ref和out的使用与区别|python基础教程|python入门|python教程
  7. 阿里云张建锋:数字技术要服务好实体经济
  8. java工厂模式应用场景_详解Java设计模式之《简单工厂模式》
  9. EXCEL表格中数字金额很大时后面零很多,如何设置直接以万元为单位显示,不显示后面的零
  10. 《开源安全运维平台OSSIM最佳实践》媒体推荐
  11. 安装了多个java 如何切换java版本
  12. DSP学习(5)—— Timer的使用
  13. Bypass一款不错的分流抢票助手工具
  14. java读取配置文件详解
  15. WPF MVVM设计模式下 相同Xaml绑定不同ViewModel问题
  16. 关于项目外包的一些总结
  17. php中电话号码输入框,php中固定电话号码和手机号码正则表达式验证
  18. CentOS使用yum命令安装软件失败,报错“Couldn‘t open file /data/ceph/ceph/repodata/repomd.xml“
  19. html量子效果,HTML5 量子谐振子动画模拟
  20. 使用C#为SAP2000开发第一个插件

热门文章

  1. MySQL · myrocks · myrocks统计信息
  2. [裴礼文数学分析中的典型问题与方法习题参考解答]5.1.5
  3. android 蓝牙通讯编程 备忘
  4. JS中的prototype
  5. 使用JConsole监控ActiveMQ
  6. 向IIS注册ASP.NET代码
  7. Sublime Text 常用插件和快捷键
  8. iOS UIScreen详解
  9. 实现线程之间的参数传递
  10. 问题-Delphi2007编译时提示内存错误“sxs.dll. No Debug Info.ACCESS 0xXXXXX