├── LICENSE ├── test └── index.html ├── README.md └── state_machine.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 https://github.com/ccqgithub 2 | All rights reserved. 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMachine Test 6 | 7 | 8 | 9 | 10 | 126 | 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StateMachine: Javascript 有限状态机 2 | == 3 | 4 | ## 什么是有限状态机? 5 | 6 | > 有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。 7 | 8 | 更详细的信息可以自行百度。 9 | 10 | ## 为什么要用状态机? 11 | 12 | 世间的一切,都可以看成是各种状态的集合。比如一块石头,今天可能是干的,明天可能是湿的。再比如一个人,年龄会变,可以从结婚变成未结婚,可以从男人变成女人,等等等…… 13 | 14 | 而状态改变的时候,我们可能要做某种事情来应对这种改变,比如结婚了想喊一嗓子,让所有人都恭喜我…… 15 | 16 | 在Javascript编程上,用到状态机的场合比比皆是。比如一个前端组件的状态,可能从‘隐藏’到‘显示’,从‘左边’到‘右边’,背景从‘白’到‘黑’,里面的图片从‘10’张到‘20张’等等…… 17 | 18 | 当组件从隐藏到显示的时候,你可能需要调用另一个组件来填充它的位置,这时候就需要在‘隐藏、显示’这个状态改变的时候做相应的操作。而促使这个组件改变可能有很多种情况(比如点击,或者过五秒隐藏),这时如果用状态机的话,只需要给它绑定一个状态改变事件而已…… 19 | 20 | 使用状态机,能让你的代码变得更直观、更整洁…… 21 | 22 | 当然,状态机能带来的好处远远不止这么多。 23 | 24 | ## 如何使用? 25 | 26 | > 注意:有用到es5的`Array.prototype.forEach`,`Array.prototype.filter`,`Array.prototype.indexOf`,不支持这几个函数的ie8或以下的浏览器暂时请自行解决。 27 | 28 | * 引入js 29 | 30 | 31 | 32 | * 初始化一个状态机 33 | 34 | // 假设为一个人设置一个状态机 他有年龄、姓名、是否结婚、朋友数量、是否有房,是否有车等状态属性 35 | var myState = new StateMachine({ 36 | // 年龄 37 | age: 27, 38 | // 姓名 39 | name: 'season.chen', 40 | // 是否结婚 41 | marry: false, 42 | // 朋友个数 43 | friends: 10, 44 | // 房 45 | house: false, 46 | // 车 47 | car: false 48 | }); 49 | 50 | * 绑定事件:`myState.on(name, handler, conditions)` or `myState.on(handler, conditions)` or `myState.on(handler)` 51 | 52 | * `name`: 事件的名称,便于识别,也便于解除事件时使用 53 | 54 | * `handler`: 事件处理程序 55 | 56 | handler = function(data) { 57 | // data.from: 之前的状态 58 | // data.to:之后的状态 59 | // data.changes: 改变 60 | } 61 | 62 | * `conditions`: 事件触发的条件,满足条件才会触发事件 63 | 64 | 如果condition是函数,它的返回值true,表示满足条件。如果condition是对象,对象里的所有条件都满足,才会触发事件。 65 | 66 | // 可以是函数 67 | conditions = function(from, to, changes) { 68 | // from: 之前的状态 69 | // to:之后的状态 70 | // changes: 改变 71 | } 72 | 73 | // 更多场合用对象 74 | conditions = { 75 | // 改变前的状态需满足 76 | from: { 77 | age: 22, // 可以是字符串、数字、布尔值, 78 | friends: function(from, to) {return to > 20}, // 可以是函数 79 | name: ['name1', 'name2'], // 可以是数组, 80 | name: /name1|name2/, // 可以是正则, 81 | }, 82 | 83 | // 改变后的状态需满足 84 | to: { 85 | // 同from 86 | }, 87 | 88 | // 这些状态需要改变 89 | changes: [], 90 | 91 | // 这些状态至少有一个改变 92 | anyChanges: [] 93 | } 94 | 95 | * 移除事件:`myState.off(name, handler)` or `myState.off(handler)` or `myState.off(name)` or `myState.off()` 96 | 97 | * 将状态恢复到初始化状态: `myState.reset()` 98 | 99 | * 设置状态: `myState.set(stateKey, stateValue)` or `myState.set({name: '11', age: 1})` 100 | 101 | * 获取状态: `myState.get(stateKey)` or `myState.get()` 102 | 103 | * 触发事件: `myState.trigger(name, handler)` or `myState.trigger(name)` or `myState.trigger(handler)` or `myState.trigger()` 104 | 105 | ### 例子(代码见`test/`目录) 106 | 107 | 假设为一个人设置一个状态机 108 | 109 | var myState = new StateMachine({ 110 | // 年龄 111 | age: 27, 112 | // 姓名 113 | name: 'season.chen', 114 | // 是否结婚 115 | marry: false, 116 | // 朋友个数 117 | friends: 10, 118 | // 房 119 | house: false, 120 | // 车 121 | car: false 122 | }); 123 | 124 | 年龄改变 125 | 126 | myState.on('changeEge', function(data) { 127 | console.log('哎呦,我又老了'+ (data.to.age - data.from.age) +'岁!'); 128 | }, { 129 | change: ['age'] 130 | }); 131 | 132 | myState.reset(); // 不是必须的,只是为了让例子更好理解,每次都恢复到最初状态 133 | myState.set('age', 29); 134 | 135 | 晚婚 大于30岁结婚 136 | 137 | myState.on('lateMarriage', function(form, to) { 138 | console.log('大于30岁才结婚,丢不丢人?'); 139 | }, { 140 | from: {marry: false}, 141 | to: { 142 | marry: true, 143 | age: function(from, to) { 144 | return to >= 30; 145 | } 146 | } 147 | }); 148 | 149 | myState.reset(); 150 | myState.set({ 151 | marry: true, 152 | age: 31 153 | }); 154 | 155 | 早婚 小于30岁结婚 156 | 157 | myState.on('earlyMarriage', function(form, to) { 158 | console.log('不到30就结婚了,还可以额么么哒!'); 159 | }, { 160 | from: {marry: false}, 161 | to: { 162 | marry: true, 163 | age: function(from, to) { 164 | return to < 30; 165 | } 166 | } 167 | }); 168 | 169 | myState.reset(); 170 | myState.set('marry', true); 171 | 172 | 改了名字 173 | 174 | myState.on('changeName', function(data) { 175 | console.log('我将名字改成了' + data.to.name); 176 | }, { 177 | change: ['name'] 178 | }); 179 | 180 | myState.reset(); 181 | myState.set('name', '王大锤!'); 182 | 183 | 改了个奇葩名字 184 | 185 | myState.on('changeWonderfulName', function(data) { 186 | console.log('我是不是脑子坏掉了,竟然改了个这么挫的名字:' + data.to.name); 187 | }, { 188 | to: { 189 | name: /王八蛋|傻子/ 190 | }, 191 | change: ['name'] 192 | }); 193 | 194 | myState.reset(); 195 | myState.set('name', '王八蛋1'); 196 | 197 | 30岁前,买车、买房,还结了婚,我从此走向了人生巅峰! 198 | 199 | myState.on(' toThePinnacle', function(data) { 200 | console.log('哈哈哈!哈哈哈!我从此走向了人生巅峰!'); 201 | }, function(from, to, changes) { 202 | if ( 203 | // 触发因子:年龄、车、房、结婚任何一个改变,都可能触发事件 204 | ( 205 | changes.indexOf('age') !== -1 206 | || changes.indexOf('car') !== -1 207 | || changes.indexOf('house') !== -1 208 | || changes.indexOf('marry') !== -1 209 | ) 210 | // 触发条件 211 | && (to.age < 30 && to.car && to.house && to.marry) 212 | ) { 213 | return true; 214 | } 215 | 216 | return false; 217 | }); 218 | 219 | myState.reset(); 220 | myState.set({ 221 | age: 28, 222 | house: true, 223 | car: true 224 | }); 225 | myState.set({ 226 | marry: true 227 | }); 228 | 229 | ## todo 230 | 231 | - 重写 232 | -------------------------------------------------------------------------------- /state_machine.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * 有限状态机 5 | */ 6 | 7 | var StateMachine = function(states) { 8 | 9 | if (this._isEmptyObj(states)) { 10 | throw new Error('请正确设置状态!'); 11 | return; 12 | } 13 | 14 | this._events = {}; 15 | this._states = states; 16 | this._initStates = this._cloneStates(); 17 | } 18 | 19 | /** 20 | * 将状态重置到初始化状态 21 | */ 22 | StateMachine.prototype.reset = function() { 23 | for (var key in this._initStates) { 24 | this._states[key] = this._initStates[key]; 25 | } 26 | } 27 | 28 | /** 29 | * 设置状态 30 | */ 31 | StateMachine.prototype.set = function(stateKey, stateValue) { 32 | var lasteStates = this._cloneStates(); 33 | var key; 34 | 35 | if (typeof stateKey == 'object') { 36 | for (key in stateKey) { 37 | this._states[key] = stateKey[key]; 38 | } 39 | } 40 | 41 | this._states[stateKey] = stateValue; 42 | 43 | // 是否触发事件 44 | return this._check(lasteStates); 45 | } 46 | 47 | /** 48 | * 获取当前状态 49 | */ 50 | StateMachine.prototype.get = function(stateKey) { 51 | var curStates = this._cloneStates(); 52 | if (!arguments.length) return curStates; 53 | return curStates[stateKey]; 54 | } 55 | 56 | /** 57 | * 绑定状态改变事件 58 | */ 59 | StateMachine.prototype.on = function(name, handler, conditions) { 60 | // .on(fn, options) 61 | if (typeof name == 'function') { 62 | conditions = handler; 63 | handler = name; 64 | name = 'default'; 65 | } 66 | 67 | if (!this._events[name]) this._events[name] = []; 68 | this._events[name].push({ 69 | handler: handler, 70 | conditions: conditions 71 | }); 72 | } 73 | 74 | /** 75 | * 取消绑定事件 76 | */ 77 | StateMachine.prototype.off = function(name, handler) { 78 | // .off(); off all 79 | if (arguments.length == 0) { 80 | this._events = {}; 81 | return; 82 | } 83 | 84 | // .off(handler); 85 | if (typeof name == 'function') { 86 | this._offByHandler(name); 87 | return; 88 | } 89 | 90 | // no event for the name 91 | if (!this._events[name]) return; 92 | 93 | // .off(name) 94 | if (typeof handler != 'function') { 95 | delete this._events[name]; 96 | return; 97 | } 98 | 99 | // .off(name, handler) 100 | this._events[name].filter(function(item) { 101 | return item.handler !== handler; 102 | }); 103 | } 104 | 105 | StateMachine.prototype._offByHandler = function(handler) { 106 | for (var name in this._events) { 107 | this._events[name].filter(function(item) { 108 | return item.handler !== handler; 109 | }); 110 | } 111 | } 112 | 113 | // 触发事件 114 | StateMachine.prototype.trigger = function(name, handler) { 115 | // trigger all 116 | if (arguments.length == 0) { 117 | this._triggerByHandler('*'); 118 | return; 119 | } 120 | 121 | // .trigger(handler) 122 | if (typeof name == 'function') { 123 | this._triggerByHandler(name); 124 | return; 125 | } 126 | 127 | if (!this._events[name]) return; 128 | 129 | var curStates = this._cloneStates(); 130 | this._events[name].forEach(function(item) { 131 | if (item.handler === handler) { 132 | item.handler.call(that, { 133 | name: name, 134 | handler: item.handler, 135 | from: curStates, 136 | to: curStates 137 | }); 138 | } 139 | }); 140 | } 141 | 142 | StateMachine.prototype._triggerByHandler = function(handler) { 143 | var curStates = this._cloneStates(); 144 | var name; 145 | var that = this; 146 | 147 | for (name in this._events) { 148 | this._events[name].forEach(function(item) { 149 | if (handler == '*' || item.handler === handler) { 150 | item.handler.call(that, { 151 | name: name, 152 | handler: item.handler, 153 | from: curStates, 154 | to: curStates 155 | }); 156 | } 157 | }); 158 | } 159 | } 160 | 161 | // 检查状态改变情况 162 | StateMachine.prototype._check = function(lastStates) { 163 | var changes = []; 164 | var triggers = []; 165 | var name, key; 166 | var that = this; 167 | 168 | for (key in this._states) { 169 | if (lastStates[key] != this._states[key]) { 170 | changes.push(key); 171 | } 172 | } 173 | 174 | // 状态没有发生改变,不会自动触发事件 175 | if (!changes.length) return false; 176 | 177 | for (name in that._events) { 178 | that._events[name].forEach(function(item) { 179 | var flag = false; 180 | 181 | if (typeof item.conditions == 'function') { 182 | flag = item.conditions(lastStates, that._cloneStates(), changes); 183 | } else if ( that._checkFrom(item.conditions, lastStates, changes) 184 | && that._checkTo(item.conditions, that._states, changes) 185 | && that._change(item.conditions, that._states, changes) 186 | && that._anyChange(item.conditions, that._states, changes) ) { 187 | flag = true; 188 | } 189 | // save event to trigger 190 | flag && triggers.push({ 191 | name: name, 192 | handler: item.handler, 193 | from: lastStates, 194 | to: that._cloneStates() 195 | }); 196 | }); 197 | } 198 | 199 | // 统一触发事件 200 | triggers.forEach(function(item) { 201 | item.handler.call(that, item); 202 | }); 203 | 204 | return true; 205 | } 206 | 207 | StateMachine.prototype._checkFrom = function(conditions, lastStates, changes) { 208 | if (this._isEmptyObj(conditions)) return true; 209 | return this._checkOneCondition(conditions['from'], lastStates, changes, lastStates); 210 | } 211 | 212 | StateMachine.prototype._checkTo = function(conditions, lastStates, changes) { 213 | if (this._isEmptyObj(conditions)) return true; 214 | return this._checkOneCondition(conditions['to'], this._states, changes, lastStates); 215 | } 216 | 217 | StateMachine.prototype._change = function(conditions, lastStates, changes) { 218 | var flag = true; 219 | 220 | if (this._isEmptyObj(conditions)) return true; 221 | if (!Array.isArray(conditions.change)) return true; 222 | conditions.change.forEach(function(v) { 223 | if (!~changes.indexOf(v)) flag = false; 224 | }); 225 | return flag; 226 | } 227 | 228 | StateMachine.prototype._anyChange = function(conditions, lastStates, changes) { 229 | var flag = false; 230 | 231 | if (this._isEmptyObj(conditions)) return true; 232 | if (!Array.isArray(conditions.anyChange)) return true; 233 | conditions.anyChange.forEach(function(v) { 234 | if (~changes.indexOf(v)) flag = true; 235 | }); 236 | return flag; 237 | } 238 | 239 | StateMachine.prototype._checkOneCondition = function(condition, compareStates, changes, lastStates) { 240 | var curStates = this._cloneStates(); 241 | 242 | if (this._isEmptyObj(condition)) return true; 243 | 244 | var flag = true; 245 | var key, cond, type; 246 | 247 | for (key in condition) { 248 | cond = condition[key]; 249 | type = this._getConditionType(cond); 250 | 251 | switch (type) { 252 | case 'function': 253 | flag = cond(lastStates[key], curStates[key]); 254 | break; 255 | case 'string': 256 | case 'other': 257 | flag = compareStates[key] === cond; 258 | break; 259 | case 'array': 260 | flag = ~cond.indexOf(compareStates[key]); 261 | break; 262 | case 'regexp': 263 | flag = cond.test(compareStates[key]); 264 | break; 265 | default: 266 | flag = false; 267 | } 268 | if (!flag) break; 269 | } 270 | return flag; 271 | } 272 | 273 | // 判断条件的类型:function,string,array,regexp 274 | StateMachine.prototype._getConditionType = function(obj) { 275 | var type = 'other'; 276 | 277 | if (typeof obj == 'function') { 278 | type = 'function'; 279 | } else if (typeof obj == 'string') { 280 | type = 'string'; 281 | } else if (typeof obj == 'object') { 282 | if (Array.isArray(obj)) { 283 | type = 'array'; 284 | } else if (obj instanceof RegExp) { 285 | type = 'regexp'; 286 | } 287 | } 288 | 289 | return type; 290 | } 291 | 292 | StateMachine.prototype._isEmptyObj = function(obj) { 293 | if (typeof obj != 'object') return true; 294 | for (var i in obj) return false; 295 | return true; 296 | } 297 | 298 | // 复制当前状态 299 | StateMachine.prototype._cloneStates = function(toCloneStates) { 300 | var states = {}; 301 | var name; 302 | 303 | toCloneStates = toCloneStates || this._states; 304 | 305 | for (name in this._states) { 306 | states[name] = this._states[name]; 307 | } 308 | 309 | return states; 310 | } 311 | --------------------------------------------------------------------------------