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

React中Stockfish.js切换FEN后仍计算旧位置的解决方法

解决Stockfish引擎切换FEN时的旧位置评估问题

核心问题分析

每次FEN变化时你都新建了一个Engine实例,旧的Worker进程并未被终止,仍在后台分析旧位置并发送消息,导致新旧引擎的消息同时触发状态更新,最终显示旧位置的评估结果。同时,直接调用stop()后没有正确重置引擎状态,也没有清理旧的消息监听。

解决方案步骤

  1. 复用单个引擎实例:用useRef存储引擎实例,避免每次FEN变化都创建新Worker。
  2. 清理旧的计算与监听:在useEffect的清理函数中停止当前搜索、移除消息监听,防止旧引擎的消息干扰新状态。
  3. 严格控制引擎流程:每次FEN更新时,先停止当前搜索,再发送新的位置命令和搜索指令,确保引擎切换到新位置。

修改后的代码

1. 优化Engine类(engine.js)

添加移除消息监听的方法,方便清理旧监听:

class Engine {
  constructor() {
    this.stockfish = new Worker("/stockfish.js");
    this.messageListener = null; // 存储监听函数,用于后续移除
    this.stockfish.postMessage("uci");
    this.stockfish.postMessage("isready");
  }

  onMessage(callback) {
    this.messageListener = e => {
      const data = e.data;
      const depthMatch = data?.match(/info depth (\d+)/);
      const depth = depthMatch ? parseInt(depthMatch[1]) : null;
      const cpMatch = data?.match(/score cp (-?\d+)/);
      const cp = cpMatch ? parseInt(cpMatch[1]) : null;
      const bestMove = data?.match(/bestmove\s+(\S+)/)?.[1];
      callback({ data, depth, cp, bestMove });
    };
    this.stockfish.addEventListener("message", this.messageListener);
  }

  removeMessageListener() {
    if (this.messageListener) {
      this.stockfish.removeEventListener("message", this.messageListener);
      this.messageListener = null;
    }
  }

  evaluatePosition(fen, depth) {
    this.stockfish.postMessage(`position fen ${fen}`);
    this.stockfish.postMessage(`go depth ${depth}`);
  }

  newGame() {
    this.stockfish.postMessage("ucinewgame");
  }

  stop() {
    this.stockfish.postMessage("stop");
  }

  quit() {
    this.stockfish.postMessage("quit");
    this.stockfish.terminate(); // 彻底终止Worker进程
  }
}

export default Engine;

2. 修改EvalBar组件(EvalBar.js)

使用useRef复用引擎,在清理函数中处理旧引擎的残留逻辑:

import { useEffect, useState, useRef } from "react";
import Engine from "@/utils/engine";

export default function EvalBar({ fen, on, turn }) {
  const [depth, setDepth] = useState(0);
  const [evalNum, setEvalNum] = useState(0);
  const [barWidth, setBarWidth] = useState(164);
  const engineRef = useRef(null); // 存储引擎实例,实现跨渲染周期复用

  useEffect(() => {
    if (!on) return; // 评估关闭时直接返回

    // 初始化引擎(仅第一次渲染时创建)
    if (!engineRef.current) {
      engineRef.current = new Engine();
    }
    const engine = engineRef.current;

    // 先停止当前正在进行的搜索,重置引擎状态
    engine.stop();
    engine.newGame();
    // 发送新位置的分析指令
    engine.evaluatePosition(fen, 20);

    // 设置消息监听,更新组件状态
    engine.onMessage(({ depth: newDepth, cp }) => {
      if (newDepth) {
        setDepth(newDepth);
      }
      if (cp) {
        const num = cp / 100;
        if (turn === "w") {
          setEvalNum(num.toFixed(1));
          setBarWidth(cp / 5 + 163);
        } else if (turn === "b") {
          setEvalNum((num * -1).toFixed(1));
          setBarWidth((cp * -1) / 5 + 163);
        }
      }
    });

    // 清理函数:FEN变化或组件卸载时,停止搜索并移除监听
    return () => {
      engine.stop();
      engine.removeMessageListener();
      // 若组件卸载需要彻底释放资源,可调用engine.quit()
      // engine.quit();
    };
  }, [fen, on, turn]);

  return on ? (
    <>
      <p className="eval-depth">D{depth}</p>
      <p className="eval-num">{evalNum}</p>
      <div className="black-bar"></div>
      <div className="white-bar" style={{ height: `${barWidth}px` }}></div>
      <div className="half-marker"></div>
    </>
  ) : null;
}

关键修改说明

  • useRef存储引擎:确保整个组件生命周期内只有一个Worker实例,避免多个引擎同时运行导致的消息冲突。
  • 清理函数:每次FEN变化时,先停止当前搜索,移除旧的消息监听,彻底切断旧引擎与组件状态的关联。
  • 先停止再更新位置:发送新位置指令前调用stop()和newGame(),强制引擎放弃旧位置的分析,确保资源集中在新位置上。

内容的提问来源于stack exchange,提问作者Daniel Mohebi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 15:50:01