React二十一点模拟:数组评估与useState弹窗触发的渲染问题
二十一点手牌模拟程序Ace处理与渲染问题解决
问题根源
- 总分
total是普通变量,每次组件渲染都会重新初始化,无法持久化状态,Popup里修改total也不会同步到父组件 - 在渲染流程中直接处理Ace判断(比如
alert),会导致每次组件渲染都执行该逻辑,一旦触发弹窗/状态更新,又会引发新一轮渲染,最终导致"渲染次数过多" - 未跟踪已处理的Ace,导致重复触发选择弹窗
解决方案
- 将
total改为React状态变量,确保总分能响应式更新并持久化 - 添加状态跟踪未处理的Ace(记录当前需要处理的Ace索引),避免重复触发弹窗
- 使用
useEffect监听手牌变化,当出现未处理的Ace时触发弹窗 - Popup组件通过回调函数将用户选择的点数传递回父组件,更新总分并标记该Ace已处理
修改后的代码
Home组件
import { useState, useEffect } from 'react' import Card from '../components/Card'; import Total from '../components/Total'; import Popup from '../components/Popup' import { shuffle } from '../hooks/shuffleCards' import { deckArray } from '../utils/data' export default function Home() { const startHandSize = 2 const [starterDeck, setStarterDeck] = useState(shuffle(deckArray)) const [howManyDealt, setHowManyDealt] = useState(startHandSize) const [triggerPopup, setTriggerPopup] = useState(false) const [total, setTotal] = useState(0) // 记录已处理的Ace索引,避免重复弹窗 const [processedAces, setProcessedAces] = useState([]) const [currentAceIndex, setCurrentAceIndex] = useState(null) const deal = () => { setHowManyDealt(startHandSize) setStarterDeck(shuffle(deckArray)) setTotal(0) setProcessedAces([]) setCurrentAceIndex(null) setTriggerPopup(false) } const hit = () => !bust && setHowManyDealt(prev => prev + 1) const usersCards = starterDeck.slice(-howManyDealt) const bust = total > 21; // 处理手牌变化,检查未处理的Ace useEffect(() => { // 先计算非Ace的基础总分 let baseTotal = 0 const unprocessedAces = [] usersCards.forEach((card, index) => { if (card.card !== "A") { baseTotal += card.value } else if (!processedAces.includes(index)) { unprocessedAces.push(index) } }) // 如果有未处理的Ace,触发弹窗 if (unprocessedAces.length > 0 && !triggerPopup) { setCurrentAceIndex(unprocessedAces[0]) setTriggerPopup(true) } else if (unprocessedAces.length === 0) { // 没有未处理的Ace,直接设置基础总分 setTotal(baseTotal) } }, [usersCards, processedAces, triggerPopup]) // 处理用户选择的Ace点数 const handleAceChoice = (points) => { setTotal(prev => prev + points) setProcessedAces(prev => [...prev, currentAceIndex]) setTriggerPopup(false) setCurrentAceIndex(null) } return ( <div> <button onClick={deal}>DEAL</button> <button disabled={bust} onClick={hit}>HIT</button> <button disabled={bust}>STAND</button> <Total total={total} usersCards={usersCards} /> <Card usersCards={usersCards} /> {triggerPopup && <Popup onAceChoice={handleAceChoice} />} </div> ) }
Popup组件
export default function Popup({ onAceChoice }) { return ( <div className="popup"> <div className="popup-inner"> <h4>You've got an Ace. Would you like to collect 1 or 10 points?</h4> <button className="btn-popup" onClick={() => onAceChoice(1)}>1 POINT</button> <button className="btn-popup" onClick={() => onAceChoice(10)}>10 POINTS</button> </div> </div> ) }
关键改动说明
- 新增
total状态变量,替代原普通变量,确保总分能持久化并触发组件更新 - 添加
processedAces和currentAceIndex,跟踪已处理的Ace,避免重复触发弹窗 - 将Ace处理逻辑移至
useEffect,仅在手牌或已处理Ace列表变化时执行,避免渲染阶段触发状态更新导致无限循环 - Popup不再直接修改
total,而是通过onAceChoice回调将选择的点数传递给父组件,由父组件更新状态 - 移除了原代码中直接在渲染流程里的
alert和弹窗触发逻辑,改为受控的useEffect监听
内容的提问来源于stack exchange,提问作者01nowicj
相关产品推荐
相关产品推荐

