首页 > 解决方案 > 为什么行的顺序在这里很重要?

问题描述

我正在创建一个记忆游戏,但我不明白这两行如何在代码中排序:

const timer = setTimeout(() => {
   if (picks.length === 2) {
     //this order that works
     setPicks([])
     setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));            
   }
}, 500)

const timer = setTimeout(() => {
   if (picks.length === 2) {
     //this order that doesn't work
     setCards(cards => cards.map((c) => ({ ...c, isFlipped: false }))); 
     setPicks([])           
   }
}, 500)

在第一种情况下,matched状态接收到正确的更新,所以为什么相似的卡片匹配,但在第二种情况下,matched状态似乎没有得到正确的值。

我不明白这两行的顺序如何影响matched状态

const App = () => {
  const icons = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];

  const shuffle = cards => [...cards].sort(() => Math.random() - 0.5);

  const shuffledCards = useMemo(() => {
    return shuffle(icons).map((icon, i) => ({
      icon: icon,
      id: i,
      isFlipped: false
    }));
  });

  const [picks, setPicks] = useState([]);
  const [cards, setCards] = useState(shuffledCards);
  const [matched, setMatched] = useState([]);

  const handleClick = id => {
    !picks.includes(id) && picks.length !== 2 && setPicks([...picks, id]);
  };

  useEffect(() => {
    setCards(cards =>
      cards.map((c, i) =>
        picks.includes(c.id) ? { ...c, isFlipped: true } : c
      )
    );

    const matches = cards.reduce((matches, { icon, isFlipped }) => {
      !matches[icon] && (matches[icon] = 0);
      isFlipped && matches[icon]++;

      return matches;
    }, {});

    Object.entries(matches).forEach(([icon, count]) => {
      count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
    });

    const timer = setTimeout(() => {
      if (picks.length === 2) {
        //the problem is here, that order doesn't work
        setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
        setPicks([]);
      }
    }, 500);
    return () => clearTimeout(timer);
  }, [picks]);

  return (
    <div class="game">
      <Deck
        cards={cards}
        handleClick={handleClick}
        picks={picks}
        matched={matched}
      />
    </div>
  );
};

const Deck = ({ numbers, cards, ...props }) => {
  return (
    <div class="deck">
      {cards.map((c, i) => {
        return <Card i={i} card={c} {...props} />;
      })}
    </div>
  );
};

const Card = ({ handleClick, picks, card, i, matched }) => {
  const { icon, isFlipped, id } = card;

  return (
    <div
      className={`card${isFlipped ? " flipped" : ""} ${
        matched.includes(icon) ? "matched" : ""
      }`}
      onClick={() => handleClick(id)}
    >
      <div class="front" />
      <div class="back">{icon}</div>
    </div>
  );
};

标签: javascriptreactjsreact-hooks

解决方案


问题 :

这里的主要问题是代码中的以下行位置

const matches = cards.reduce((matches, { icon, isFlipped }) => {

所以要理解这个问题,首先,我们需要了解上面的行位置有什么问题,请阅读评论以更好地理解这个问题

setCards(cards => // <--------- 1.
    cards.map((c, i) =>  // <--------- 2.
        picks.includes(c.id) ? { ...c, isFlipped: true } : c
    )
);

// 1. current state of cards
// 2. returning updated state of cards

// so, if you are aware of async behavior or setState, then you know
// the below line will always point to snapshot of cards when the useEffect run
// and not to the 1. or 2.

// So here cards is pointing to snapshot when useEffect run 
const matches = cards.reduce((matches, { icon, isFlipped }) => {

现在,让我们看看这两种情况

 useEffect(() => {

    // Here you will always get the snapshot of cards values, which is currently available

    setCards(cards => // <--------- 1.
        cards.map((c, i) =>  // <--------- 2.
            picks.includes(c.id) ? { ...c, isFlipped: true } : c
        )
    );

    // so if you understand correctly, above code block doesn't make any sense for the below line
    const matches = cards.reduce((matches, { icon, isFlipped }) => {
    ....

    const timer = setTimeout(() => {
        if (picks.length === 2) {
            //this order that works
            setPicks([]) // <---- this will trigger useEffect , and send the cards value from 2.
            setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));            
        }
    }, 500)

    const timer = setTimeout(() => {
        if (picks.length === 2) {
            //this order that works
            setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));  // <------------- 3.   
            setPicks([]) // <---- this will trigger useEffect , and send the cards value from 3.
        }
    }, 500)

 },[picks]); // <----- useEffect is dependent on picks

调试演示

您可以根据您的情况更改订单并检查控制台并查看您cards在切换订单时获得的值是什么

编辑#SO-flip-card-issue


解决方案 :

希望这能让您的疑问清晰,现在如果我们清楚问题,那么解决方案是什么,这里我们可以通过两种方式解决它,但基本的两种方式都很常见,那就是始终使用更新的卡片值,在这种情况下顺序不再重要


第一个解决方案

您可以将匹配条件代码块放入setCards(cards => {并使用最新的卡片值

  useEffect(() => {
    setCards(cards => {
      const updated = cards.map((c, i) =>
        picks.includes(c.id) ? { ...c, isFlipped: true } : c
      )

      // take whole code block and put it inside the `setCards`
      // and directly work with latest update value
      const matches = updated.reduce((matches, { icon, isFlipped }) => { // <---- HERE
        !matches[icon] && (matches[icon] = 0);
        isFlipped && matches[icon]++;

        return matches;
      }, {});

      Object.entries(matches).forEach(([icon, count]) => {
        count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
      });
      return updated;
    });

    const timer = setTimeout(() => {
      if (picks.length === 2) {
        setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
        setPicks([]);
      }
    }, 500);
    return () => clearTimeout(timer);
  }, [picks]);

工作演示

编辑#SO-flip-card-solution-1

第二个解决方案:(我建议这个)

您可以创建一个useEffectforcards值,以便您始终获得更新的cards值,并在此基础上设置匹配值

  useEffect(() => {
    setCards(cards =>
      cards.map((c, i) =>
        picks.includes(c.id) ? { ...c, isFlipped: true } : c
      )
    );

    const timer = setTimeout(() => {
      if (picks.length === 2) {
        setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
        setPicks([]);
      }
    }, 500);
    return () => clearTimeout(timer);
  }, [picks]);

  useEffect(() => {
    const matches = cards.reduce((matches, { icon, isFlipped }) => {
      !matches[icon] && (matches[icon] = 0);
      isFlipped && matches[icon]++;

      return matches;
    }, {});

    Object.entries(matches).forEach(([icon, count]) => {
      count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
    });
  }, [cards]);

工作演示

编辑#SO-flip-card-solution-2


推荐阅读