You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Recoil、类与TypeScript报错:无法为Snake对象的只读属性'positions'赋值

问题描述

我用React开发贪吃蛇游戏,想通过Recoil atom存储的类来实现控制逻辑,相关代码如下:

game_controller.ts

export const state_game_controller = atom<GameController | undefined>({
  "key": "GameController",
  "default":  undefined
})

export const useChangeSnakeDirection = () => {
  const game_controller = useRecoilValue(state_game_controller)
  return (dir:TDirections) => {
    game_controller?.changeSnakeDirection(dir)
  }
}

export const useStartGame = () => {
  const game_controller = useRecoilValue(state_game_controller)
  return () => {
    game_controller?.startGame()
  }
}

app.tsx

export const App = () => {
  const startGame = useStartGame();

  return (
    <>
      <Board />
      <button onClick={() => startGame()}>开始游戏</button>
      <Control />
    </>
  );
};

点击开始按钮后,触发如下报错:

Cannot assign to read only property 'positions' of object '#<Snake>'

游戏控制器类的属性声明如下:

private canvas: HTMLCanvasElement;
private gameboard: GameBoard
private snake: Snake
private apple: Apple
private canvas_drawer: CanvasDrawer
private loop: Loop
private score: number

我原本以为只要修改React atom的属性就能改变蛇的方向,现在遇到这个问题,求解决办法。

解决方案

报错的核心原因是Recoil会自动冻结存储的对象,防止意外的可变修改,而你的Snake类实例的positions属性被直接修改时,因为对象已被冻结,所以触发了只读属性的错误。下面是几种解决思路:

1. 临时关闭Recoil的对象冻结(仅调试用)

在atom定义中添加dangerouslyAllowMutability: true,可以关闭自动冻结机制:

export const state_game_controller = atom<GameController | undefined>({
  key: "GameController",
  default: undefined,
  dangerouslyAllowMutability: true // 添加此行
})

但这种方法违背了Recoil的状态管理设计原则,容易引发不可预测的状态变化,不建议在生产环境使用。

2. 改用不可变更新方式(推荐)

Recoil要求状态更新遵循不可变原则,你需要调整GameController和Snake的实现,每次修改状态时返回新实例,而非直接修改现有实例的属性:

  • 修改Snake类,让方向修改方法返回新的Snake实例:
class Snake {
  // ... 现有属性定义

  changeDirection(newDir: TDirections): Snake {
    // 基于原位置创建新数组,避免直接修改原数据
    const updatedPositions = [...this.positions];
    // 执行方向改变的逻辑,更新updatedPositions
    // ...
    // 返回新的Snake实例,传入更新后的属性
    return new Snake(updatedPositions, newDir, /* 其他必要属性 */);
  }
}
  • 同步调整useChangeSnakeDirection钩子,通过useSetRecoilState更新atom,返回新的GameController实例:
export const useChangeSnakeDirection = () => {
  const setGameController = useSetRecoilState(state_game_controller);
  return (dir: TDirections) => {
    setGameController(prevController => {
      if (!prevController) return prevController;
      // 获取更新后的蛇实例
      const updatedSnake = prevController.snake.changeDirection(dir);
      // 返回新的GameController实例,替换蛇对象
      return new GameController(
        prevController.canvas,
        prevController.gameboard,
        updatedSnake,
        prevController.apple,
        prevController.canvas_drawer,
        prevController.loop,
        prevController.score
      );
    });
  };
};

3. 拆分原子化状态(更优实践)

不建议将整个类实例存储在Recoil中,更好的方式是把游戏状态拆分为多个原子化的Recoil state,比如:

  • snakeState:存储蛇的位置、方向等数据
  • gameStatusState:存储游戏运行状态、分数等
  • appleState:存储苹果的位置

然后通过Recoil的selector组合这些状态,实现游戏逻辑。这种方式既符合不可变原则,也更便于调试和维护。


内容的提问来源于stack exchange,提问作者Antonio Pacheco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 19:17:44