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

