diff --git a/README.md b/README.md index e9b9cd3..b03cb18 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ A library for finite state machines. +一个有限状态机库. + +查看[中文文档](README_zh-cn.md) + ![matter state machine](examples/matter.png)
@@ -14,6 +18,9 @@ A library for finite state machines. > **VERSION 3.0** Is a significant rewrite from earlier versions. Existing 2.x users should be sure to read the [Upgrade Guide](docs/upgrading-from-v2.md). +> 值得注意的是**VERSION 3.0** 已经重写了。 + 现有2.x用户应该阅读[升级指南](docs/upgrading-from-v2.md). +
# Installation diff --git a/README_zh-cn.md b/README_zh-cn.md new file mode 100644 index 0000000..5b540e4 --- /dev/null +++ b/README_zh-cn.md @@ -0,0 +1,151 @@ +# Javascript State Machine + +[![NPM version](https://badge.fury.io/js/javascript-state-machine.svg)](https://badge.fury.io/js/javascript-state-machine) +[![Build Status](https://travis-ci.org/jakesgordon/javascript-state-machine.svg?branch=master)](https://travis-ci.org/jakesgordon/javascript-state-machine) + +一个有限状态机库. + +![matter state machine](examples/matter.png) + +
+ +### 现有用户注意 + +> 值得关注的是**VERSION 3.0** 已经重写了。 + 现有2.x用户应该阅读[升级指南](docs/upgrading-from-v2.md). + +
+ +# 安装 + +在浏览器中使用: + +```html + +``` + +> 在下载[js文件](dist/state-machine.js)或者[压缩版js文件](dist/state-machine.min.js)之后引用。 + +在Node中使用npm安装: + +```shell + npm install --save-dev javascript-state-machine + or + npm install --save javascript-state-machine +``` + +在Node的js文件中导入: + +```javascript + var StateMachine = require('javascript-state-machine'); +``` + +# 用法 + +一个状态机可以这样构建: + +```javascript + var fsm = new StateMachine({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid' }, + { name: 'freeze', from: 'liquid', to: 'solid' }, + { name: 'vaporize', from: 'liquid', to: 'gas' }, + { name: 'condense', from: 'gas', to: 'liquid' } + ], + methods: { + onMelt: function() { console.log('I melted') }, + onFreeze: function() { console.log('I froze') }, + onVaporize: function() { console.log('I vaporized') }, + onCondense: function() { console.log('I condensed') } + } + }); +``` + +... 创建的对象上有包含当前状态的的属性: + + * `fsm.state` + +... 创建的对象上有转换到不同状态的方法: + + * `fsm.melt()` + * `fsm.freeze()` + * `fsm.vaporize()` + * `fsm.condense()` + +... 观察方法在生命周期中自动被调用: + + * `onMelt()` + * `onFreeze()` + * `onVaporize()` + * `onCondense()` + +... 还有下面的帮助方法: + +|方法|注释| +|---|---| +|fsm.is(s)|如果当前状态`s`是当前状态则返回true| +|fsm.can(t)|如果转换`t`在当前状态下`可以`发生则返回true| +|fsm.cannot(t)|如果转换`t`在当前状态下`不可以`发生则返回true| +|fsm.transitions()|返回当前状态下可以发生的转换的列表| +|fsm.allTransitions()|返回所有可以发生的转换的列表| +|fsm.allStates()|返回所有可以出现的状态的列表| + +# 术语 + +一个状态机由一组[**States状态**](docs/states-and-transitions.md)组成 + + * solid + * liquid + * gas + +一个状态机可以通过[**Transitions转换**](docs/states-and-transitions.md)改变状态 + + * melt + * freeze + * vaporize + * condense + +一个状态机在转换期间可以通过观察[**Lifecycle Events生命周期事件**](docs/lifecycle-events.md)执行操作 + + * onBeforeMelt + * onAfterMelt + * onLeaveSolid + * onEnterLiquid + * ... + +一个状态机可以有任意的[**Data 数据 and Methods 方法**](docs/data-and-methods.md). + +多个状态机实例可以通过使用[**State Machine Factory**](docs/state-machine-factory.md)来创建. + +# 文档 + +阅读更多有关的文档 + + * [States and Transitions状态和转换](docs/zh-cn/states-and-transitions.md) + * [Data and Methods数据和方法](docs/zh-cn/data-and-methods.md) + * [Lifecycle Events生命周期事件](docs/zh-cn/lifecycle-events.md) + * [Asynchronous Transitions异步转换](docs/zh-cn/async-transitions.md) + * [Initialization初始化](docs/zh-cn/initialization.md) + * [Error Handling错误处理](docs/zh-cn/error-handling.md) + * [State History状态历史](docs/zh-cn/state-history.md) + * [Visualization可视化](docs/zh-cn/visualization.md) + * [State Machine Factory状态机工厂](docs/state-machine-factory.md) + * [Upgrading from 2.x从2.x升级](docs/zh-cn/upgrading-from-v2.md) + +# 贡献(〜^㉨^)〜 + +你可以通过创建issues或者pr来给项目[贡献](docs/contributing.md) + +# 发行记录 + +查看 [发行记录](RELEASE_NOTES.md) 文件. + +# 协议 + +查看[MIT协议](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) 文件. + +# 联系 + +如果你有想法, 反馈, 需求 或者bugs报告, 你可以联系我 +[jake@codeincomplete.com](mailto:jake@codeincomplete.com), 或通过我的网站: [Code inComplete](http://codeincomplete.com/) diff --git a/docs/zh-cn/async-transitions.md b/docs/zh-cn/async-transitions.md new file mode 100644 index 0000000..0ef9889 --- /dev/null +++ b/docs/zh-cn/async-transitions.md @@ -0,0 +1,54 @@ +# 异步转换 + +在阅读本文之前,您应该先熟悉状态机[生命周期事件](lifecycle-events.md)。 + +有时,您需要在状态转换期间执行一些异步代码,并确保直到代码完成,才进入下一状态。 + +例如:希望逐渐淡化UI组件后改变状态,在动画完成前不要转换到下一个状态,在动画完成后转换到下一个状态。 +你可以通过从任意[生命周期事件](lifecycle-events.md)中返回[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/._Objects/Promise)对象 + +从生命周期事件返回`Promise`将导致该转变的生命周期暂停。它可以通过解决`Promise`来继续,或者通过拒绝`Promise`来取消。 + +例如(使用jQuery的效果): + +```javascript + var fsm = new StateMachine({ + + init: 'menu', + + transitions: [ + { name: 'play', from: 'menu', to: 'game' }, + { name: 'quit', from: 'game', to: 'menu' } + ], + + methods: { + + onEnterMenu: function() { + return new Promise(function(resolve, reject) { + $('#menu').fadeIn('fast', resolve) + }) + }, + + onEnterGame: function() { + return new Promise(function(resolve, reject) { + $('#game').fadeIn('fast', resolve) + }) + }, + + onLeaveMenu: function() { + return new Promise(function(resolve, reject) { + $('#menu').fadeOut('fast', resolve) + }) + }, + + onLeaveGame: function() { + return new Promise(function(resolve, reject) { + $('#game').fadeOut('fast', resolve) + }) + } + } + + }) +``` + +> 确保你最终总是解决(或拒绝)你的`Promise`,否则状态机将永远停留在那个挂起的转换中。 \ No newline at end of file diff --git a/docs/zh-cn/contributing.md b/docs/zh-cn/contributing.md new file mode 100644 index 0000000..01e5e74 --- /dev/null +++ b/docs/zh-cn/contributing.md @@ -0,0 +1,59 @@ +# Contributing + +The `javascript-state-machine` library is built using: + + * [Webpack 2](https://webpack.js.org/concepts/) - for bundling javascript modules together + * [UglifyJS2](https://github.com/mishoo/UglifyJS2) - for minifying bundled javascript files + * [Ava](https://github.com/avajs/ava) - for testing + +The directory structure includes: + +```shell + /bin # - build scripts + /dist # - minified bundles for distribution + /docs # - documentation + /examples # - example visualizations + /lib # - bundled source code for npm + /src # - source code + /test # - unit tests + + package.json # - npm configuration + webpack.config.js # - webpack configuration + + LICENSE # - the project licensing terms + README.md # - the project readme + RELEASE_NOTES.md # - the project release notes + +``` + +Build time dependencies can be installed using npm: + +```shell + > npm install +``` + +A number of npm scripts are available: + +```shell + > npm run test # run unit tests + > npm run build # bundle and minify files for distribution + > npm run watch # run tests if source files change +``` + +## Source Code + +The source code is written in es5 syntax and should be supported by all [es5 compatible browsers](http://caniuse.com/#feat=es5). +[Babel](https://babeljs.io/) is **NOT** used for this project. Webpack is used to +bundle modules together for distribution. + +## Submitting Pull Requests + +Generally speaking, please raise an issue first and lets discuss the problem and the +proposed solution. The next step would be a pull-request - fantastic and thank you for helping out - but +please try to... + + * ensure the tests pass (`npm test`). + * rebuild distribution files (`npm run build`). + * include tests for your changes. + * include documentation for your changes. + * include a great commit message. diff --git a/docs/zh-cn/data-and-methods.md b/docs/zh-cn/data-and-methods.md new file mode 100644 index 0000000..7b91462 --- /dev/null +++ b/docs/zh-cn/data-and-methods.md @@ -0,0 +1,62 @@ +# 数据和函数 + +除了[状态和转换](states-and-transitions.md),一个状态机也可以包含任意的数据和方法。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + data: { + color: 'red' + }, + methods: { + describe: function() { + console.log('I am ' + this.color); + } + } + }); + + fsm.state; // 'A' + fsm.color; // 'red' + fsm.describe(); // 'I am red' +``` + +## 数据和状态机工厂 + +如果你想通过[状态机工厂](state-machine-factory.md)创建多个实例,那么`data`对象将会在多个实例之间共享,这可能不是你想要的。为确保每一个实例获得唯一的`data`你应该使用`data`函数代替。 + +```javascript + var FSM = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + data: function(color) { // <-- use a method that can be called for each instance + return { + color: color + } + }, + methods: { + describe: function() { + console.log('I am ' + this.color); + } + } + }); + + var a = new FSM('red'), + b = new FSM('blue'); + + a.state; // 'A' + b.state; // 'A' + + a.color; // 'red' + b.color; // 'blue' + + a.describe(); // 'I am red' + b.describe(); // 'I am blue' +``` + +> 注意: 在构造每个实例时使用的参数直接传递到“data”函数。 + diff --git a/docs/zh-cn/error-handling.md b/docs/zh-cn/error-handling.md new file mode 100644 index 0000000..c301f65 --- /dev/null +++ b/docs/zh-cn/error-handling.md @@ -0,0 +1,52 @@ +# 错误处理 + +## 无效转换 + +默认情况下,如果尝试触发当前状态中不允许的转换,则状态机将引发异常。如果您喜欢自己处理这个问题,可以定义一个自定义的“onInvalidTransition”处理程序: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'reset', from: 'B', to: 'A' } + ], + methods: { + onInvalidTransition: function(transition, from, to) { + throw new Exception("transition not allowed from that state"); + } + } + }); + + fsm.state; // 'A' + fsm.can('step'); // true + fsm.can('reset'); // false + + fsm.reset(); // <-- throws "transition not allowed from that state" +``` + +## 悬而未决的转换 + +默认情况下,如果在[Lifecycle Event生命周期事件](lifecycle-events.md)期间为挂起的转换尝试触发其他转换,则状态机将抛出异常。如果你喜欢自己处理这个问题,你可以定义一个自定义的“onPendingTransition”处理程序: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ], + methods: { + onLeaveA: function() { + this.step(); // <-- uh oh, trying to transition from within a lifecycle event is not allowed + }, + onPendingTransition: function(transition, from, to) { + throw new Exception("transition already in progress"); + } + } + }); + + fsm.state; // 'A' + fsm.can('step'), // true + fsm.step(); // <-- throws "transition already in progress" +``` diff --git a/docs/zh-cn/initialization.md b/docs/zh-cn/initialization.md new file mode 100644 index 0000000..a71c686 --- /dev/null +++ b/docs/zh-cn/initialization.md @@ -0,0 +1,55 @@ +# 初始化选项 + +## 显式初始化 + +默认情况下,如果你不特别的初始化状态,状态机将会处于`none`状态,在创建期间没有生命周期事件触发,并且你需要提供一个显示转换来跳出这个状态。 + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'init', from: 'none', to: 'A' }, + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); + fsm.state; // 'none' + fsm.init(); // 'init()' transition is fired explicitly + fsm.state; // 'A' +``` + +## 隐式初始化 + +如果你指定初始状态的名称,那么将会在创建状态机的时候创建隐式的转换并触发(还会触发响应的生命周期事件) + +>这是最常见的初始化策略,绝大多数(90%)时候你应该使用这个方法。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); // 'init()' transition fires from 'none' to 'A' during construction + fsm.state; // 'A' +``` + +## 用状态机工厂初始化 + +For [State Machine Factories](state-machine-factory.md), the `init` transition +is triggered for each constructed instance. + + [State Machine Factories状态机工厂](state-machine-factory.md),为每个状态机触发初始化转换。 + +```javascript + var FSM = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); + + var fsm1 = new FSM(), // 'init()' transition fires from 'none' to 'A' for fsm1 + fsm2 = new FSM(); // 'init()' transition fires from 'none' to 'A' for fsm2 +``` diff --git a/docs/zh-cn/lifecycle-events.md b/docs/zh-cn/lifecycle-events.md new file mode 100644 index 0000000..adfe268 --- /dev/null +++ b/docs/zh-cn/lifecycle-events.md @@ -0,0 +1,148 @@ +# 生命周期事件 + +当转换发生时,为了追踪或执行某些动作,可以观察五个通用的生命周期事件: + + * `onBeforeTransition` - 转换之前调用 + * `onLeaveState` - 离开一个状态时调用 + * `onTransition` - 转换期间调用 + * `onEnterState` - 进入一个状态时调用 + * `onAfterTransition` - 转换之后调用 + +In addition to the general-purpose events, transitions can be observed +using your specific transition and state names: + +除了通用事件之外,还可以使用特定的转换和状态名称来观察转换: + + * `onBefore` - 在一个特定的`TRANSITION`转换开始之前 + * `onLeave` - 在离开一个特定的`STATE`状态时调用 + * `onEnter` - 在进入一个特定的`STATE`状态时调用 + * `onAfter` - 在一个特定的`TRANSITION`转换之后调用 + +为了便利起见,两个最有用的事件可以缩短为: + + * `on` - 等价于`onAfter` + * `on` - 等价于`onEnter` + +## 观察生命周期事件 + +个体生命周期事件可以`observe()`方法观察到 + +```javascript + fsm.observe('onStep', function() { + console.log('stepped'); + }); +``` + +可以使用观察者对象观察多个事件 + +```javascript + fsm.observe({ + onStep: function() { console.log('stepped'); } + onA: function() { console.log('entered state A'); } + onB: function() { console.log('entered state B'); } + }); +``` + +一个状态机总是观察它自己的生命周期事件。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + methods: { + onStep: function() { console.log('stepped'); } + onA: function() { console.log('entered state A'); } + onB: function() { console.log('entered state B'); } + } + }); +``` + +## 生命周期事件参数 + +观察者将传递一个包含“生命周期”对象的单个参数,该属性具有以下属性 + + * **transition** - 转换名称 + * **from** - 先前的状态 + * **to** - 接下来的状态 + +除了“生命周期”参数,观察者还将接收传递到转换方法中的其他参数。 + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + methods: { + onTransition: function(lifecycle, arg1, arg2) { + console.log(lifecycle.transition); // 'step' + console.log(lifecycle.from); // 'A' + console.log(lifecycle.to); // 'B' + console.log(arg1); // 42 + console.log(arg2); // 'hello' + } + } + }); + + fsm.step(42, 'hello'); +``` + +## 生命周期事件名称 + +Lifecycle event names always use standard javascipt camelCase, even if your transition and +state names do not: +生命周期事件名称总是使用标准的js驼峰命名法,即使你的转换和状态名称不是按照驼峰命名法命名的: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'do-with-dash', from: 'has-dash', to: 'has_underscore' }, + { name: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized' }, + { name: 'doAlreadyCamelized', from: 'alreadyCamelize', to: 'has-dash' } + ], + methods: { + onBeforeDoWithDash: function() { /* ... */ }, + onBeforeDoWithUnderscore: function() { /* ... */ }, + onBeforeDoAlreadyCamelized: function() { /* ... */ }, + onLeaveHasDash: function() { /* ... */ }, + onLeaveHasUnderscore: function() { /* ... */ }, + onLeaveAlreadyCamelized: function() { /* ... */ }, + onEnterHasDash: function() { /* ... */ }, + onEnterHasUnderscore: function() { /* ... */ }, + onEnterAlreadyCamelized: function() { /* ... */ }, + onAfterDoWithDash: function() { /* ... */ }, + onAfterDoWithUnderscore: function() { /* ... */ }, + onAfterDoAlreadyCamelized: function() { /* ... */ } + } + }); +``` + +# 按顺序列出的生命周期事件 + +重申,转换生命周期按以下顺序发生: + + * `onBeforeTransition` - fired before any transition + * `onBefore` - fired before a specific TRANSITION + * `onLeaveState` - fired when leaving any state + * `onLeave` - fired when leaving a specific STATE + * `onTransition` - fired during any transition + * `onEnterState` - fired when entering any state + * `onEnter` - fired when entering a specific STATE + * `on` - convenience shorthand for `onEnter` + * `onAfterTransition` - fired after any transition + * `onAfter` - fired after a specific TRANSITION + * `on` - convenience shorthand for `onAfter` + +# 取消转换 + +任何观察者可以通过在任何生命周期事件中显式地返回“false”来取消转换: + + * `onBeforeTransition` + * `onBefore` + * `onLeaveState` + * `onLeave` + * `onTransition` + +所有后续生命周期事件将被取消,状态将保持不变。 + diff --git a/docs/zh-cn/state-history.md b/docs/zh-cn/state-history.md new file mode 100644 index 0000000..3f4f5a2 --- /dev/null +++ b/docs/zh-cn/state-history.md @@ -0,0 +1,123 @@ +# 状态历史 + +默认情况下,一个状态机只追踪当前状态,如果希望追踪状态历史,你可以用`state-machine-history`插件扩展状态机。 + +```javascript + var StateMachineHistory = require('javascript-state-machine/lib/history') +``` + +```javascript + + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' } + ], + plugins: [ + new StateMachineHistory() // <-- plugin enabled here + ] + }) + + fsm.history; // [ 'A' ] + fsm.step(); + fsm.history; // [ 'A', 'B' ] + fsm.step(); + fsm.history; // [ 'A', 'B', 'C' ] + + fsm.clearHistory(); + + fsm.history; // [ ] + +``` +## 穿越历史 + +你可以使用`historyBack`和`historyForward`穿越历史。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' } + ] + }) + + fsm.step(); + fsm.step(); + fsm.step(); + + fsm.state; // 'D' + fsm.history; // [ 'A', 'B', 'C', 'D' ] + + fsm.historyBack(); + + fsm.state; // 'C' + fsm.history; // [ 'A', 'B', 'C' ] + + fsm.historyBack(); + + fsm.state; // 'B' + fsm.history; // [ 'A', 'B' ] + + fsm.historyForward(); + + fsm.state; // 'C' + fsm.history; // [ 'A', 'B', 'C' ] +``` + +你可以使用下面方法测试穿越历史是否被允许: + +```javascript + fsm.canHistoryBack; // true/false + fsm.canHistoryForward; // true/false +``` + +一组 [Lifecycle Events生命周期事件](lifecycle-events.md) 将仍旧在穿越历史时应用。 + +## 限制历史记录数量 + +默认情况下,状态机的历史知道被清除是无限增长的。你可以通过配置来限制只存储最后的N条状态。 + +``` javascript + var fsm = new StateMachine({ + plugins: [ + new StateMachineHistory({ max: 100 }) // <-- plugin configuration + ] + }) +``` + +## 自定义历史 + +如果`history`和你存在的状态机属性或方法发生术语冲突,你可以用一个不同的名字打开`state-machine-history`插件。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' } + ], + plugins: [ + new StateMachineHistory({ name: 'memory' }) + ] + }) + + fsm.step(); + fsm.step(); + + fsm.memory; // [ 'A', 'B', 'C' ] + + fsm.memoryBack(); + fsm.memory; // [ 'A', 'B' ] + + fsm.memoryForward(); + fsm.memory; // [ 'A', 'B', 'C' ] + + fsm.clearMemory(); + fsm.memory; // [ ] +``` + diff --git a/docs/zh-cn/state-machine-factory.md b/docs/zh-cn/state-machine-factory.md new file mode 100644 index 0000000..f484f33 --- /dev/null +++ b/docs/zh-cn/state-machine-factory.md @@ -0,0 +1,98 @@ +# 状态机工厂 + +此文档中的大多数示例构造了单个状态机实例,例如: + +```javascript + var fsm = new StateMachine({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid' }, + { name: 'freeze', from: 'liquid', to: 'solid' }, + { name: 'vaporize', from: 'liquid', to: 'gas' }, + { name: 'condense', from: 'gas', to: 'liquid' } + ] + }); +``` + +如果你希望创建用相同的配置创建多个状态机,你应该使用状态机工厂。状态机工厂提供可以用来多次实例化的JS构造函数。 + +```javascript + var Matter = StateMachine.factory({ // <-- the factory is constructed here + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid' }, + { name: 'freeze', from: 'liquid', to: 'solid' }, + { name: 'vaporize', from: 'liquid', to: 'gas' }, + { name: 'condense', from: 'gas', to: 'liquid' } + ] + }); + + var a = new Matter(), // <-- instances are constructed here + b = new Matter(), + c = new Matter(); + + b.melt(); + c.melt(); + c.vaporize(); + + a.state; // solid + b.state; // liquid + c.state; // gas +``` + +使用这个工厂,每一个状态机实例都有一个唯一的实例。每一个状态机管理他自己的`state`属性,但是方法是被通过JS原型机制(原型链)共享的。 + +> 注意:了解特殊案例处理需求 [Data and State Machine Factories数据和状态机工厂](data-and-methods.md#data-and-state-machine-factories) + +## 应用状态机工厂行为到现有对象 + +有时,您可能希望将状态机行为应用到已经存在的对象(例如,反作用组件)。你可以用“StateMachine.apply”方法来实现这一点: + +```javascript + var component = { /* ... */ }; + + StateMachine.apply(component, { + init: 'A', + transitions: { + { name: 'step', from: 'A', to: 'B' } + } + }); +``` + +> 小心的使用状态和转换名称,否则可能会和对象属性冲突。 + +## 应用状态机工厂行为到现有的类 + +还可以将状态机工厂行为应用于现有类,但是现在必须通过在类构造函数方法中调用`this._fsm()'来负责初始化: + +```javascript + function Person(name) { + this.name = name; + this._fsm(); // <-- IMPORTANT + } + + Person.prototype = { + speak: function() { + console.log('my name is ' + this.name + ' and I am ' + this.state); + } + } + + StateMachine.factory(Person, { + init: 'idle', + transitions: { + { name: 'sleep', from: 'idle', to: 'sleeping' }, + { name: 'wake', from: 'sleeping', to: 'idle' } + } + }); + + var amy = new Person('amy'), + bob = new Person('bob'); + + bob.sleep(); + + amy.state; // 'idle' + bob.state; // 'sleeping' + + amy.speak(); // 'my name is amy and I am idle' + bob.speak(); // 'my name is bob and I am sleeping' +``` diff --git a/docs/zh-cn/states-and-transitions.md b/docs/zh-cn/states-and-transitions.md new file mode 100644 index 0000000..fe5b7bb --- /dev/null +++ b/docs/zh-cn/states-and-transitions.md @@ -0,0 +1,156 @@ +# 状态和转换 + +![matter state machine](../../examples/matter.png) + +一个状态机由一组**States状态**组成, 例如: + + * solid + * liquid + * gas + +.. 和一组**转换**, 例如: + + * melt + * freeze + * vaporize + * condense + +```javascript + var fsm = new StateMachine({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid' }, + { name: 'freeze', from: 'liquid', to: 'solid' }, + { name: 'vaporize', from: 'liquid', to: 'gas' }, + { name: 'condense', from: 'gas', to: 'liquid' } + ] + }); + + fsm.state; // 'solid' + fsm.melt(); + fsm.state; // 'liquid' + fsm.vaporize(); + fsm.state; // 'gas' +``` + +## 多种状态和转换 + +![wizard state machine](../../examples/wizard.png) + +如果一个转换被允许`从`多种状态开始,那么用同一个名称定义这些转换。 + +```javascript + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' } +``` + +如果一个转换是从多个不同的状态开始被转换到相同的状态,例如: + +```javascript + { name: 'reset', from: 'B', to: 'A' }, + { name: 'reset', from: 'C', to: 'A' }, + { name: 'reset', from: 'D', to: 'A' } +``` + +... 那么他可以被简写为从`数组`状态开始: + +```javascript + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A' } +``` + +把他们组合到一起,例如: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' }, + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A' } + ] + }) +``` + +这个例子将会创建一个有两个转换方法的对象: + + * `fsm.step()` + * `fsm.reset()` + +`reset`转换将总是转换到`A`状态, `step`转换的结果取决于当前的状态. + +## 通配符转换 + +If a transition is appropriate from **any** state, then a wildcard '*' `from` state can be used: + +如果一个转换可以从任意状态开始,那么可以使用通配符`*`。 + +```javascript + var fsm = new StateMachine({ + transitions: [ + // ... + { name: 'reset', from: '*', to: 'A' } + ] + }); +``` + +## 条件转换 + +一个转换可以通过提供一个函数到`to`属性来在运行时选择目标状态。 + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: '*', to: function(n) { return increaseCharacter(this.state, n || 1) } } + ] + }); + + fsm.state; // A + fsm.step(); + fsm.state; // B + fsm.step(5); + fsm.state; // G + + // helper method to perform (c = c + n) on the 1st character in str + function increaseCharacter(str, n) { + return String.fromCharCode(str.charCodeAt(0) + n); + } +``` + +`allStates`方法的返回列表只会包含运行时已经出现过的状态。 + +```javascript + fsm.state; // A + fsm.allStates(); // [ 'A' ] + fsm.step(); + fsm.state; // B + fsm.allStates(); // [ 'A', 'B' ] + fsm.step(5); + fsm.state; // G + fsm.allStates(); // [ 'A', 'B', 'G' ] +``` + +## 跳转 - `不通过转换改变状态` + +你可以使用条件转换构造出一个`跳转`转换。 + +```javascript + var fsm = new StateMachine({ + init: 'A' + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' }, + { name: 'goto', from: '*', to: function(s) { return s } } + ] + }) + + fsm.state; // 'A' + fsm.goto('D'); + fsm.state; // 'D' +``` + +一组完整的[Lifecycle Events生命周期事件](lifecycle-events.md)仍旧在应用`goto`时可用. + diff --git a/docs/zh-cn/upgrading-from-v2.md b/docs/zh-cn/upgrading-from-v2.md new file mode 100644 index 0000000..42f8150 --- /dev/null +++ b/docs/zh-cn/upgrading-from-v2.md @@ -0,0 +1,378 @@ +# Upgrading from Version 2.x + +Version 3.0 is a significant rewrite from earlier versions in order to support more +advanced use cases and to improve the existing use cases. Unfortunately, many of these +updates are incompatible with earlier versions, so changes are required in your code when you upgrade +to version 3.x. We want to tackle those all in one swoop and avoid any more big-bang changes +in the future. + +Please read this article carefully if you are upgrading from version 2.x to 3.x. + +> A [summary](#upgrade-summary) of the changes required can be found at the end of the article. + +### Table of Contents + + * [**Construction**](#construction) - constructing single instances follows a more idomatic javascript pattern. + * [**State Machine Factory**](#state-machine-factory) - constructing multiple instances from a class has been simplified. + * [**Data and Methods**](#data-and-methods) - A state machine can now have additional data and methods. + * [**Renamed Terminology**](#renamed-terminology) - A more consistent terminology has been applied. + * [**Lifecycle Events**](#lifecycle-events) - (previously called 'callbacks') are camelCased and observable. + * [**Async Transitions**](#promise-based-asynchronous-transitions) - Asynchronous transitions now use standard Promises. + * [**Conditional Transitions**](#conditional-transitions) - A transition can now dynamically choose its target state at run-time. + * [**Goto**](#goto) - The state can be changed without a defined transition using `goto`. + * [**State History**](#state-history) - The state history can now be retained and traversed with back/forward semantics. + * [**Visualization**](#visualization) - A state machine can now be visualized using GraphViz. + * [**Build System**](#build-system) - A new webpack-based build system has been implemented. + +## Construction + +Constructing a single state machine now follows a more idiomatic javascript pattern: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ /* ... */ }) +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ /* ... */ }) // <-- more idomatic +``` + +## State Machine Factory + +Constructing multiple instances from a state machine 'class' has been simplified: + +Version 2.x: + +```javascript + function FSM() { } + + StateMachine.create({ + target: FSM.prototype, + // ... + }) + + var a = new FSM(), + b = new FSM(); +``` + +**Version 3.x**: + +```javascript + var FSM = StateMachine.factory({ /* ... */ }), // <-- generate a factory (a constructor function) + a = new FSM(), // <-- then create instances + b = new FSM(); +``` + +## Data and Methods + +A state machine can now have additional (arbitrary) data and methods defined: + +Version 2.x: _not supported_. + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + data: { + color: 'red' + }, + methods: { + speak: function() { console.log('hello') } + } + }); + + fsm.color; // 'red' + fsm.speak(); // 'hello' +``` + +## Renamed Terminology + +A more consistent terminology has been applied: + + * A state machine consists of a set of [**States**](states-and-transitions.md). + * A state machine changes state by using [**Transitions**](states-and-transitions.md). + * A state machine can perform actions during a transition by observing [**Lifecycle Events**](lifecycle-events.md). + * A state machine can also have arbitrary [**Data and Methods**](data-and-methods.md). + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + initial: 'ready', + events: [ /* ... */ ], + callbacks: { /* ... */ } + }); + + fsm.current; // 'ready' +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + init: 'ready', // <-- renamed s/initial/init/ + transitions: [ /* ... */ ], // <-- renamed s/events/transitions/ + data: { /* ... */ }, // <-- new + methods: { /* ... */ } // <-- renamed s/callbacks/methods/ + // ... which can contain arbitrary methods AND lifecycle event callbacks + }); + + fsm.state; // 'ready' // <-- renamed s/current/state/ +``` + +## Lifecycle Events + +**Callbacks** have been renamed **Lifecycle Events** and are now declared as `methods` on the +state machine using a more traditional javascript camelCase for the method names: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + initial: 'initial-state', + events: [ + { name: 'do-something', from: 'initial-state', to: 'final-state' } + ], + callbacks: { + onbeforedosomething: function() { /* ... */ }, + onleaveinitialstate: function() { /* ... */ }, + onenterfinalstate: function() { /* ... */ }, + onafterdosomething: function() { /* ... */ } + } + }) +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + init: 'initial-state', + transitions: [ + { name: 'do-something', from: 'initial-state', to: 'final-state' } + ], + methods: { // <-- renamed s/callbacks/methods/ + onBeforeDoSomething: function() { /* ... */ }, // <-- camelCase naming convention + onLeaveInitialState: function() { /* ... */ }, // <-- + onEnterFinalState: function() { /* ... */ }, // <-- + onAfterDoSomething: function() { /* ... */ } // <-- + } + }) +``` + +
+Lifecycle events are now passed information in a single `lifecycle` argument: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + events: [ + { name: 'step', from: 'none', to: 'complete' } + ], + callbacks: { + onbeforestep: function(event, from, to) { + console.log('event: ' + event); // 'step' + console.log('from: ' + from); // 'none' + console.log('to: ' + to); // 'complete' + }, + } + }); +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'none', to: 'complete' } + ], + methods: { + onBeforeStep: function(lifecycle) { // <-- combined into a single argument + console.log('transition: ' + lifecycle.transition); // 'step' + console.log('from: ' + lifecycle.from); // 'none' + console.log('to: ' + lifecycle.to); // 'complete' + } + } + }); +``` + +> This change allows us to include additional information in the future without having to have a ridiculous +number of arguments to lifecycle event observer methods + +
+Lifecycle events are also now observable by others: + +Version 2.x: _not supported_. + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ /* ... */ }); + + // observe individual lifecycle events with observer methods + fsm.observe('onBeforeTransition', function() { /* ... */ }); + fsm.observe('onLeaveState', function() { /* ... */ }); + + // or observe multiple lifecycle events with an observer object + fsm.observe({ + onBeforeTransition: function() { /* ... */ }, + onLeaveState: function() { /* ... */ } + }); +``` + +
+The general purpose lifecycle events now use the word `transition` instead of `event` and +occur **before** their specialized versions: + +Version 2.x, the lifecycle order was: + + * `onbefore` + * `onbeforeevent` + * `onleave` + * `onleavestate` + * `onenter` + * `onenterstate` + * `on` + * `onafter` + * `onafterevent` + * `on` + +**Version 3.x**, the lifecycle order is: + + * `onBeforeTransition` - fired before any transition + * `onBefore` - fired before a specific TRANSITION + * `onLeaveState` - fired when leaving any state + * `onLeave` - fired when leaving a specific STATE + * `onTransition` - fired during any transition + * `onEnterState` - fired when entering any state + * `onEnter` - fired when entering a specific STATE + * `on` - convenience shorthand for `onEnter` + * `onAfterTransition` - fired after any transition + * `onAfter` - fired after a specific TRANSITION + * `on` - convenience shorthand for `onAfter` + +> For more details, read [Lifecycle Events](lifecycle-events.md) + +## Promise-Based Asynchronous Transitions + +Asynchronous transitions are now implemented using standard javascript [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + +If you return a Promise from **any** lifecycle event then the entire lifecycle for that transition +is put on hold until that Promise gets resolved. If the promise is rejected then the transition +is cancelled. + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + events: [ + { name: 'step', from: 'none', to: 'complete' } + ], + callbacks: { + onbeforestep: function() { + $('#ui').fadeOut('fast', function() { + fsm.transition(); + }); + return StateMachine.ASYNC; + } + } + }); +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'none', to: 'complete' } + ], + methods: { + onBeforeStep: function() { + return new Promise(function(resolve, reject) { // <-- return a Promise instead of StateMachine.ASYNC + $('#ui').fadeOut('fast', resolve); // <-- resolve the promise instead of calling .transition() + }); + } + } + }); +``` + +> For more details, read [Asynchronous Transitions](async-transitions.md) + +## Conditional Transitions + +A transition can now be conditional and choose the target state at run-time by providing a function +as the `to` attribute. + +Version 2.x: _not supported_. + +**Version 3.x**: See [Conditional Transitions](states-and-transitions.md#conditional-transitions) + +## Goto + +The state can now be changed without the need for a predefined transition using a conditional `goto` +transition: + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Goto](states-and-transitions.md#goto---changing-state-without-a-transition) + +## State History + +A state machine can now track and traverse (back/forward) its state history. + +Version 2.x: _not supported_. + +**Version 3.x**: See [State History](state-history.md) + +## Visualization + +A state machine can now be visualized as a directed graph using GraphViz `.dot` syntax. + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Visualization](visualization.md) + +## Build System + +A new [Webpack](https://webpack.js.org/concepts/) based build system has been provided along +with an [Ava](https://github.com/avajs/ava) based unit test suite. + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Contributing](contributing.md) + +## Other Breaking Changes in Version 3.0 + +`isFinished` is no longer built-in, you can easily add it to your state machine with a custom method: + +```javascript + var fsm = new StateMachine({ + methods: { + isFinished: function() { return this.state === 'done' } + } + }) +``` + +# UPGRADE SUMMARY + +The following list summarizes the above changes you might need when upgrading to version 3.0 + + * replace `StateMachine.create()` with `new StateMachine()` + * rename: + * `initial` to `init` + * `events` to `transitions` + * `callbacks` to `methods` + * `fsm.current` to `fsm.state` + * update your callback methods: + * rename them to use traditional javascript `camelCasing` + * refactor them to use the single `lifecycle` argument instead of individual `event,from,to` arguments + * update any asynchronous callback methods: + * return a `Promise` instead of `StateMachine.ASYNC` + * `resolve()` the promise when ready instead of calling `fsm.transition()` + * replace `StateMachine.create({ target: FOO })` with: + * if FOO is a class - `StateMachine.factory(FOO, {})` + * if FOO is an object - `StateMachine.apply(FOO, {})` + diff --git a/docs/zh-cn/visualization.md b/docs/zh-cn/visualization.md new file mode 100644 index 0000000..65695ad --- /dev/null +++ b/docs/zh-cn/visualization.md @@ -0,0 +1,213 @@ +# 可视化 + +想流程图一样可视化你的状态机可能非常有用,如果我们使用`visualize`方法转化我们的状态机配置到`.dot`语言,那么可以使用开源的[GraphViz](http://www.graphviz.org/)库来可视化。 + +```javascript + var visualize = require('javascript-state-machine/lib/visualize'); + + var fsm = new StateMachine({ + init: 'open', + transitions: [ + { name: 'close', from: 'open', to: 'closed' }, + { name: 'open', from: 'closed', to: 'open' } + ] + }); + + visualize(fsm) + console.log(visualize(fsm)); +``` + +生成下面的 .dot 语句: + +```dot + digraph "fsm" { + "closed"; + "open"; + "closed" -> "open" [ label=" open " ]; + "open" -> "closed" [ label=" close " ]; + } +``` + +GraphViz会像这样显示: + +![door](../../examples/vertical_door.png) + +## 增强显示 + +You can customize the generated `.dot` output - and hence the graphviz visualization - by attaching +`dot` attributes to your transitions and (optionally) declaring an `orientation`: + +```javascript + var fsm = new StateMachine({ + init: 'closed', + transitions: [ + { name: 'open', from: 'closed', to: 'open', dot: { color: 'blue', headport: 'n', tailport: 'n' } }, + { name: 'close', from: 'open', to: 'closed', dot: { color: 'red', headport: 's', tailport: 's' } } + ] + }); + visualize(fsm, { name: 'door', orientation: 'horizontal' }); + console.log(visualize(fsm, { name: 'door', orientation: 'horizontal' })); +``` + +生成下面的.dot 语句: + +```dot + digraph "door" { + rankdir=LR; + "closed"; + "open"; + "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ]; + "open" -> "closed" [ color="red" ; headport="s" ; label=" close " ; tailport="s" ]; + } +``` + +GraphViz会像这样显示: + +![door](../../examples/horizontal_door.png) + +## 可视化状态机工厂 + +You can use the same `visualize` method to generate `.dot` output for a state machine factory: + +```javascript + var Matter = StateMachine.factory({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } }, + { name: 'freeze', from: 'liquid', to: 'solid', dot: { headport: 'se' } }, + { name: 'vaporize', from: 'liquid', to: 'gas', dot: { headport: 'nw' } }, + { name: 'condense', from: 'gas', to: 'liquid', dot: { headport: 'se' } } + ] + }); + + visualize(Matter, { name: 'matter', orientation: 'horizontal' }) + console.log(visualize(Matter, { name: 'matter', orientation: 'horizontal' })) +``` + +生成下面的.dot 语句: + +```dot + digraph "matter" { + rankdir=LR; + "solid"; + "liquid"; + "gas"; + "solid" -> "liquid" [ headport="nw" ; label=" melt " ]; + "liquid" -> "solid" [ headport="se" ; label=" freeze " ]; + "liquid" -> "gas" [ headport="nw" ; label=" vaporize " ]; + "gas" -> "liquid" [ headport="se" ; label=" condense " ]; + } +``` + +GraphViz会像这样显示: + +![matter](../../examples/matter.png) + +## 其他示例 + +```javascript + var Wizard = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B', dot: { headport: 'w', tailport: 'ne' } }, + { name: 'step', from: 'B', to: 'C', dot: { headport: 'w', tailport: 'e' } }, + { name: 'step', from: 'C', to: 'D', dot: { headport: 'w', tailport: 'e' } }, + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A', dot: { headport: 'se', tailport: 's' } } + ] + }); + + visualize(Wizard, { orientation: 'horizontal' }) + console.log(visualize(Wizard, { orientation: 'horizontal' })) +``` + +生成下面的.dot 语句: + +```dot + digraph "wizard" { + rankdir=LR; + "A"; + "B"; + "C"; + "D"; + "A" -> "B" [ headport="w" ; label=" step " ; tailport="ne" ]; + "B" -> "C" [ headport="w" ; label=" step " ; tailport="e" ]; + "C" -> "D" [ headport="w" ; label=" step " ; tailport="e" ]; + "B" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "C" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "D" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + } +``` + +GraphViz会像这样显示: + +![wizard](../../examples/wizard.png) + +```javascript + var ATM = StateMachine.factory({ + init: 'ready', + transitions: [ + { name: 'insert-card', from: 'ready', to: 'pin' }, + { name: 'confirm', from: 'pin', to: 'action' }, + { name: 'reject', from: 'pin', to: 'return-card' }, + { name: 'withdraw', from: 'return-card', to: 'ready' }, + + { name: 'deposit', from: 'action', to: 'deposit-account' }, + { name: 'provide', from: 'deposit-account', to: 'deposit-amount' }, + { name: 'provide', from: 'deposit-amount', to: 'confirm-deposit' }, + { name: 'confirm', from: 'confirm-deposit', to: 'collect-envelope' }, + { name: 'provide', from: 'collect-envelope', to: 'continue' }, + + { name: 'withdraw', from: 'action', to: 'withdrawal-account' }, + { name: 'provide', from: 'withdrawal-account', to: 'withdrawal-amount' }, + { name: 'provide', from: 'withdrawal-amount', to: 'confirm-withdrawal' }, + { name: 'confirm', from: 'confirm-withdrawal', to: 'dispense-cash' }, + { name: 'withdraw', from: 'dispense-cash', to: 'continue' }, + + { name: 'continue', from: 'continue', to: 'action' }, + { name: 'finish', from: 'continue', to: 'return-card' } + ] + }) + + visualize(ATM) + console.log(visualize(ATM)) +``` + +生成下面的.dot 语句: + +```dot + digraph "ATM" { + "ready"; + "pin"; + "action"; + "return-card"; + "deposit-account"; + "deposit-amount"; + "confirm-deposit"; + "collect-envelope"; + "continue"; + "withdrawal-account"; + "withdrawal-amount"; + "confirm-withdrawal"; + "dispense-cash"; + "ready" -> "pin" [ label=" insert-card " ]; + "pin" -> "action" [ label=" confirm " ]; + "pin" -> "return-card" [ label=" reject " ]; + "return-card" -> "ready" [ label=" withdraw " ]; + "action" -> "deposit-account" [ label=" deposit " ]; + "deposit-account" -> "deposit-amount" [ label=" provide " ]; + "deposit-amount" -> "confirm-deposit" [ label=" provide " ]; + "confirm-deposit" -> "collect-envelope" [ label=" confirm " ]; + "collect-envelope" -> "continue" [ label=" provide " ]; + "action" -> "withdrawal-account" [ label=" withdraw " ]; + "withdrawal-account" -> "withdrawal-amount" [ label=" provide " ]; + "withdrawal-amount" -> "confirm-withdrawal" [ label=" provide " ]; + "confirm-withdrawal" -> "dispense-cash" [ label=" confirm " ]; + "dispense-cash" -> "continue" [ label=" withdraw " ]; + "continue" -> "action" [ label=" continue " ]; + "continue" -> "return-card" [ label=" finish " ]; + } +``` + +GraphViz会像这样显示: + +![atm](../../examples/atm.png)