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

React中使用Fisher-Yates算法时数组莫名截断的问题求助

React中Apollo数据洗牌数组截断问题的分析与解决

核心原因推测

  • Apollo Client返回的数组是只读代理对象:为了保证缓存一致性,Apollo会把返回数据包装成不可变结构,直接执行Fisher-Yates里的交换操作会破坏原结构,导致数组异常截断。
  • 状态/引用误用:用useState存洗牌逻辑会触发重复渲染,触发React的无限循环防护;useRef直接存Apollo原数组时,因为是只读代理,修改操作会静默失败或损坏数据结构。

正确的Fisher-Yates洗牌实现

不管用哪种写法,第一步必须拷贝原数组,脱离Apollo的只读代理:

解构拷贝基础版

// 先拷贝原数组,避免操作只读数据
const shuffledArr = [...originalData];
for (let i = shuffledArr.length - 1; i > 0; i--) {
  const j = Math.floor(Math.random() * (i + 1));
  [shuffledArr[i], shuffledArr[j]] = [shuffledArr[j], shuffledArr[i]];
}

reduce实现版

const shuffledArr = originalData.reduce((acc, _, idx) => {
  const randomIdx = Math.floor(Math.random() * (idx + 1));
  [acc[idx], acc[randomIdx]] = [acc[randomIdx], acc[idx]];
  return acc;
}, [...originalData]); // 初始值用拷贝后的数组

非渲染数据的存储方案

如果数组仅用于API交互、不需要触发渲染:

  • 使用useRef时,先拷贝原数组再存入,避开只读限制:
const dataRef = useRef([]);

useEffect(() => {
  if (originalData) {
    // 拷贝后再处理
    const copiedArr = [...originalData];
    // 执行洗牌
    for (let i = copiedArr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [copiedArr[i], copiedArr[j]] = [copiedArr[j], copiedArr[i]];
    }
    dataRef.current = copiedArr;
  }
}, [originalData]);

关于洗牌索引的绕过方案

你用的洗牌索引再映射原数组的方式完全可行,本质是绕开了直接修改原数组的操作,自然避开了只读代理的限制:

const indices = Array.from({ length: originalData.length }, (_, i) => i);
// 洗牌索引数组
for (let i = indices.length - 1; i > 0; i--) {
  const j = Math.floor(Math.random() * (i + 1));
  [indices[i], indices[j]] = [indices[j], indices[i]];
}
// 通过索引映射得到洗牌后的数组
const shuffledArr = indices.map(idx => originalData[idx]);

无法复现异常的说明

这种异常大概率是操作只读代理导致的数据结构损坏,当代码环境变化(比如Apollo缓存策略调整、React渲染周期变化)时,可能不再触发,但核心问题还是没有意识到第三方库返回的数据可能是只读的。后续处理这类数据时,养成先拷贝再修改的习惯就能避免类似问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 03:43:19