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

React归并排序可视化useState不显示中间步骤及状态更新报错求解

问题诱因
  • 闭包导致状态快照过时:初始调用mergeSort时传入的mainArray是当时的状态快照,后续所有递归调用中拿到的mainArray都停留在初始值,并非最新的实时状态。基于旧值拼接的中间数组内容错误,甚至出现元素缺失、结构异常,最终导致展示区域空白。
  • 缺失子数组位置标记:当前mergeSort仅处理子数组的内容,没有记录这段子数组在原数组中的起止位置,拼接新数组时默认把合并结果放在最前面,覆盖了其他已经排好序的片段,中间态数组结构完全不符合预期,自然无法正确展示。你看到的“没有触发渲染”只是假象,实际上渲染的是错误的中间数组内容,直到最后一次全数组合并完成,才会显示正确的最终排序结果。
  • 异步任务未做清理:归并排序的异步任务链很长,如果你在排序过程中重新触发排序、或者切换页面导致组件卸载,旧的异步任务仍然会继续执行,调用已卸载组件的setArr就会触发你看到的警告,也可能进一步导致状态更新混乱。
  • 关于所谓的useState懒更新:你的理解存在偏差,useState的批量更新仅在同步执行场景下生效,你代码中所有await之后的setArr都会触发独立渲染,不需要用JSON.parse(JSON.stringify())做冗余深拷贝,展开运算符已经足够生成新的引用触发渲染。
解决方案

1. 调整mergeSort参数,新增位置标记

给mergeSort增加当前处理子数组在原数组中的起始索引start、结束索引end,合并完成后可以精准替换原数组对应位置的内容,不会覆盖其他已排序片段。

2. 使用函数式更新获取最新状态

setArr改用函数式写法,参数默认是当前最新的数组状态,彻底解决闭包拿到旧状态的问题,不需要再传入mainArray参数。

3. 增加异步任务中断逻辑

用useRef存储运行标记,组件卸载或者重新排序时中断旧的异步任务,避免更新已卸载组件的状态。

修改后的代码示例

组件内调用逻辑

const [arr, setArr] = useState(/* 你的初始数组 */)
const isRunningRef = useRef(false)
const DELAY = 50 // 你自己定义的动画间隔

// 触发排序的函数
const handleStartSort = async () => {
  // 每次排序前终止上一次的任务
  isRunningRef.current = false
  // 等待一小段时间让旧任务全部终止
  await delay(DELAY + 10)
  isRunningRef.current = true
  // 初始调用传入整个数组的起止索引 0 到 arr.length-1
  await mergeSort([...arr], 0, arr.length - 1, setArr, isRunningRef)
}

// 清理函数,组件卸载时终止任务
useEffect(() => {
  return () => {
    isRunningRef.current = false
  }
}, [])

调整后的mergeSort函数

async function mergeSort(arr, start, end, setArr, isRunningRef) {
  let n = arr.length;
  if (n === 1) {
    return arr;
  }
  const mid = Math.floor(n / 2);
  let leftArray = arr.slice(0, mid);
  let rightArray = arr.slice(mid, n);

  leftArray = await mergeSort(leftArray, start, start + mid - 1, setArr, isRunningRef);
  rightArray = await mergeSort(rightArray, start + mid, end, setArr, isRunningRef);

  let mergeResultArray = await merge(leftArray, rightArray, isRunningRef);
  
  // 如果已经终止运行就直接返回,不更新状态
  if (!isRunningRef.current) return mergeResultArray;

  // 函数式更新,拿到最新的数组状态,精准替换对应区间的内容
  setArr(prevArr => {
    const newArr = [...prevArr];
    // 把合并结果替换到原数组对应的[start, end]区间
    for (let i = 0; i < mergeResultArray.length; i++) {
      newArr[start + i] = mergeResultArray[i];
    }
    return newArr;
  });

  return mergeResultArray;
}

调整后的merge函数

async function merge(array1, array2, isRunningRef) {
  let array3 = [];

  while (array1.length && array2.length) {
    await delay(DELAY);
    // 终止时直接返回剩余内容
    if (!isRunningRef.current) return array3.concat(array1, array2);
    if (array1[0].val > array2[0].val) {
      array3.push(array2[0]);
      array2.splice(0, 1);
    } else {
      array3.push(array1[0]);
      array1.splice(0, 1);
    }
  }

  while (array1.length) {
    await delay(DELAY);
    if (!isRunningRef.current) return array3.concat(array1);
    array3.push(array1[0]);
    array1.splice(0, 1);
  }

  while (array2.length) {
    await delay(DELAY);
    if (!isRunningRef.current) return array3.concat(array2);
    array3.push(array2[0]);
    array2.splice(0, 1);
  }

  return array3;
}

原有delay函数无需修改

const delay = (DELAY) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("");
    }, DELAY);
  });
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 20:27:03