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
相关产品推荐
相关产品推荐

