JS `Generator` 用于实现状态机:通过 `yield` 控制状态流转

各位靓仔靓女,晚上好!我是你们今晚的JS状态机特约讲师,老司机带你用Generator飙车!

今天咱们聊聊一个听起来高大上,但其实玩起来贼有意思的东西:用JS的Generator实现状态机。保证让你听完之后,感觉自己一下子从青铜跳到王者,代码写得飞起!

啥是状态机?

简单来说,状态机就是描述一个对象在不同状态之间如何转换的模型。 想象一下红绿灯,它有三种状态:红灯、黄灯、绿灯。 状态之间有明确的转换规则,比如绿灯变黄灯,黄灯变红灯,红灯变绿灯。

状态机这玩意儿,在很多地方都有用武之地:

  • 游戏开发: 角色状态(待机、行走、攻击、死亡),AI行为等等。
  • UI开发: 组件状态(显示、隐藏、加载中、错误)。
  • 工作流引擎: 任务状态(待处理、处理中、已完成、已拒绝)。
  • 网络协议: TCP连接状态(已连接、等待数据、已关闭)。

为啥用Generator?

你可能会问,实现状态机的方法多了去了,为啥非得用Generator呢? 理由如下:

  1. 代码更清晰,更易读: Generator可以将每个状态的逻辑独立出来,代码结构更清晰,更容易理解和维护。
  2. 控制流程更灵活: 通过yield关键字,可以精确控制状态的转换,想什么时候切换就什么时候切换。
  3. 状态切换更自然: Generator的暂停和恢复执行的特性,使得状态切换更加自然,避免了回调地狱。

Generator的基本概念

在开始之前,咱们先复习一下Generator的基本概念。 如果你已经很熟悉了,可以直接跳过。

  • Generator函数:function*声明的函数,与普通函数不同的是,它可以暂停执行,并在稍后恢复执行。
  • yield关键字: 用于在Generator函数中暂停执行,并将一个值返回给调用者。
  • next()方法: 用于恢复Generator函数的执行,并传递一个值给Generator函数。
function* myGenerator() {
  console.log("开始执行");
  yield 1;
  console.log("执行到yield 1");
  yield 2;
  console.log("执行到yield 2");
  return 3;
}

const gen = myGenerator();

console.log(gen.next()); // 输出: 开始执行 { value: 1, done: false }
console.log(gen.next()); // 输出: 执行到yield 1 { value: 2, done: false }
console.log(gen.next()); // 输出: 执行到yield 2 { value: 3, done: true }
console.log(gen.next()); // 输出: { value: undefined, done: true }

状态机实战:简单的交通灯

咱们先来一个最简单的例子:用Generator实现一个交通灯状态机。

function* trafficLight() {
  while (true) {
    console.log("红灯");
    yield "red";
    console.log("绿灯");
    yield "green";
    console.log("黄灯");
    yield "yellow";
  }
}

const light = trafficLight();

console.log(light.next()); // 输出: 红灯 { value: 'red', done: false }
console.log(light.next()); // 输出: 绿灯 { value: 'green', done: false }
console.log(light.next()); // 输出: 黄灯 { value: 'yellow', done: false }
console.log(light.next()); // 输出: 红灯 { value: 'red', done: false }
// ... 循环往复

这个例子很简单,但它已经展示了Generator如何控制状态的转换。 每次调用light.next(),交通灯就会切换到下一个状态。

状态机进阶:带参数的状态转换

上面的例子只能按照固定的顺序切换状态,不够灵活。 咱们来一个更高级的例子:带参数的状态转换。

假设我们要实现一个简单的电梯状态机。 电梯有以下状态:

  • idle: 待机状态
  • moving: 移动状态
  • stopped: 停止状态

电梯的状态转换规则如下:

  • 从idle状态可以移动到moving状态,需要指定目标楼层。
  • 从moving状态可以移动到stopped状态,到达目标楼层后自动停止。
  • 从stopped状态可以移动到idle状态,或者移动到moving状态。
function* elevator() {
  let currentFloor = 1; // 初始楼层
  let targetFloor = null; // 目标楼层
  let state = 'idle'; // 初始状态

  while (true) {
    switch (state) {
      case 'idle':
        console.log(`电梯处于待机状态,当前楼层:${currentFloor}`);
        targetFloor = yield { state: 'idle', floor: currentFloor }; // 等待目标楼层
        if (targetFloor !== null && targetFloor !== undefined && targetFloor !== currentFloor) {
          state = 'moving'; // 切换到移动状态
        }
        break;
      case 'moving':
        console.log(`电梯正在移动,当前楼层:${currentFloor},目标楼层:${targetFloor}`);
        // 模拟电梯移动
        while (currentFloor !== targetFloor) {
          currentFloor += (targetFloor > currentFloor) ? 1 : -1;
          console.log(`电梯到达 ${currentFloor} 层`);
          yield { state: 'moving', floor: currentFloor, target: targetFloor }; // 每次移动都 yield
        }
        state = 'stopped'; // 切换到停止状态
        break;
      case 'stopped':
        console.log(`电梯已停止,当前楼层:${currentFloor}`);
        yield { state: 'stopped', floor: currentFloor }; // 停止状态
        state = 'idle'; // 切换到待机状态
        break;
      default:
        console.log("未知状态");
        return;
    }
  }
}

const myElevator = elevator();

// 启动电梯
console.log(myElevator.next()); // 输出: 电梯处于待机状态,当前楼层:1 { value: { state: 'idle', floor: 1 }, done: false }

// 去5楼
console.log(myElevator.next(5)); // 输出: 电梯正在移动,当前楼层:1,目标楼层:5 { value: { state: 'moving', floor: 2, target: 5 }, done: false }
console.log(myElevator.next()); // 输出: 电梯到达 2 层 { value: { state: 'moving', floor: 3, target: 5 }, done: false }
console.log(myElevator.next()); // 输出: 电梯到达 3 层 { value: { state: 'moving', floor: 4, target: 5 }, done: false }
console.log(myElevator.next()); // 输出: 电梯到达 4 层 { value: { state: 'moving', floor: 5, target: 5 }, done: false }
console.log(myElevator.next()); // 输出: 电梯已停止,当前楼层:5 { value: { state: 'stopped', floor: 5 }, done: false }
console.log(myElevator.next()); // 输出: 电梯处于待机状态,当前楼层:5 { value: { state: 'idle', floor: 5 }, done: false }

// 再次去1楼
console.log(myElevator.next(1));

这个例子稍微复杂一些,但它展示了如何使用next()方法传递参数给Generator函数,从而控制状态的转换。

状态机设计原则

在设计状态机时,需要遵循一些基本原则,才能保证状态机的正确性和可维护性。

  • 明确状态: 首先要明确系统有哪些状态。
  • 定义转换规则: 明确状态之间如何转换,以及转换的条件。
  • 避免循环依赖: 状态之间的转换关系应该是有向的,避免出现循环依赖。
  • 处理异常情况: 考虑各种异常情况,并定义相应的处理机制。

状态机模式:状态模式

状态模式是一种行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为。 状态模式的核心思想是将状态封装成独立的类,并将状态的转换委托给Context对象。

// 状态接口
class State {
  constructor(name) {
    this.name = name;
  }
  enter() {
    console.log(`进入 ${this.name} 状态`);
  }
  execute() {
    console.log(`执行 ${this.name} 状态`);
  }
  exit() {
    console.log(`退出 ${this.name} 状态`);
  }
}

// 具体状态类
class IdleState extends State {
  constructor() {
    super("待机");
  }
  execute(context) {
    console.log("待机状态,等待指令");
    return "moving"; // 返回下一个状态
  }
}

class MovingState extends State {
  constructor() {
    super("移动");
  }
  execute(context) {
    console.log("移动状态,前往目标楼层");
    return "stopped"; // 返回下一个状态
  }
}

class StoppedState extends State {
  constructor() {
    super("停止");
  }
  execute(context) {
    console.log("停止状态,电梯已到达");
    return "idle"; // 返回下一个状态
  }
}

// 环境类
class Context {
  constructor() {
    this.states = {
      idle: new IdleState(),
      moving: new MovingState(),
      stopped: new StoppedState(),
    };
    this.currentState = this.states.idle; // 初始状态
  }

  transition(stateName) {
    if (this.states[stateName]) {
      this.currentState.exit(); // 退出当前状态
      this.currentState = this.states[stateName]; // 切换到新状态
      this.currentState.enter(); // 进入新状态
    } else {
      console.log("无效状态");
    }
  }

  run() {
    let nextState = this.currentState.execute(this); // 执行当前状态
    if (nextState) {
      this.transition(nextState); // 切换到下一个状态
    }
  }
}

// 使用
const context = new Context();
context.run(); // 待机状态,等待指令
context.run(); // 移动状态,前往目标楼层
context.run(); // 停止状态,电梯已到达
context.run(); // 待机状态,等待指令

用Generator实现状态模式

虽然状态模式通常使用类来实现,但我们也可以用Generator来实现类似的效果。

function* stateMachine() {
  let state = 'idle';

  while (true) {
    switch (state) {
      case 'idle':
        console.log("待机状态,等待指令");
        state = yield 'idle'; // 等待指令
        break;
      case 'moving':
        console.log("移动状态,前往目标楼层");
        state = yield 'moving'; // 执行移动
        break;
      case 'stopped':
        console.log("停止状态,电梯已到达");
        state = yield 'stopped'; // 停止
        break;
      default:
        console.log("无效状态");
        return;
    }
  }
}

const sm = stateMachine();
sm.next(); // 启动状态机,进入idle状态

sm.next('moving'); // 切换到moving状态
sm.next('stopped'); // 切换到stopped状态
sm.next('idle'); // 切换到idle状态

这个例子虽然简单,但它展示了如何用Generator实现状态模式的核心思想:将状态的逻辑独立出来,并通过yield关键字控制状态的转换。

状态机库

如果你不想自己手写状态机,也可以使用现成的状态机库。 比较流行的JS状态机库有:

库名称 描述
XState 功能强大的状态机库,支持分层状态、并行状态、历史状态等高级特性。
JavaScript State Machine 简单易用的状态机库,API简洁明了。
machina.js 轻量级的状态机库,专注于核心功能。

这些库都提供了丰富的功能和API,可以帮助你更方便地构建复杂的状态机。

总结

今天咱们一起学习了如何使用JS的Generator实现状态机。 相信你已经掌握了以下知识点:

  • 状态机的基本概念和应用场景。
  • Generator的基本概念和用法。
  • 如何使用Generator实现简单的状态机。
  • 如何使用Generator实现带参数的状态转换。
  • 状态机的设计原则。
  • 状态模式的核心思想。
  • 如何用Generator实现状态模式。
  • 常用的JS状态机库。

希望今天的分享对你有所帮助。 记住,理论是基础,实践才是王道。 多写代码,多练习,才能真正掌握状态机的精髓。

下次有机会,咱们再聊聊更高级的状态机技巧! 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注