🎉

about-closure

に公開

關於閉包(closure)


直接先來看一段 JS

function outerFunction() {
  let counter = 0;
  function innerFunction() {
    counter++;
    console.log("Counter:", counter);
  }
  return innerFunction;
}

const myFunction = outerFunction();
myFunction(); // Counter: 1
myFunction(); // Counter: 2
myFunction(); // Counter: 3

發生了什麼

  • 一開始在 outerFunction中宣告了 let counter = 0,
    • counterouterFunction 作用域裡的變數
    • 並且執行完後變數應該會被釋放記憶體
  • 但是因為將 innerFunction return 出去
    • return 就是告訴 JS:『哎!我外面還有人要使用這個變數』
    • JS 就沒有將 counter 的記憶體釋放
  • innerFuction 因為閉包的關係就擁有了使用 counter的權限
    • 每次執行 innerFunction 時,都可以存取並修改 counter

React 中常見的例子

function CounterComponent() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setTimeout(() => {
      console.log('Count:', count);
      setCount(count + 1);
    }, 1000);
  }

  return <button onClick={handleClick}>Add</button>;
}
  • 如果快速的點擊 3 下,會出現 3 個 0
  • 因為閉包保存的是點擊當下 count 的值,也就是一開始設定的 0
  • 所以就算有執行 setCount(count + 1),但最終還是會印出 0
Count: 0
Count: 0
Count: 0
  • 有間隔的點擊確實會正確的累加,是因為 React 已經把 setCount 更新並 render 完成
    • 但實務上無法控制使用者手賤一直點,可能會發生以下情況
實務場景 影響
送出表單 多次點擊「送出」按鈕 後端收到多次請求,產生重複資料或多次扣款!
加購物車按鈕 快速點擊 同樣商品被加入多次,數量異常上升
切換畫面按鈕 連點 路由異常多次跳轉,甚至報錯
異步操作中重複觸發事件 比如 API 請求還沒完成就又點一次,結果舊的 state 被覆蓋掉

解法

  1. Functional update form of setState
    • 使用 setCount((prevCount) => prevCount + 1)
    const App = () => {
      const [, setCount] = useState(0);
      function handleClick() {
        setTimeout(() => {
    +      setCount((prevCount) => {
            console.log('Count', prevCount);
            return prevCount + 1;
          });
        }, 1000);
      }
      return <button onClick={handleClick}>Add</button>;
    };
    
  2. 加上 disabled 狀態,按鈕送出後就先鎖定

最後に

以上為我個人的基本理解,藉由比較生活化的方式來記憶閉包的觀念,如果有什麼想法也歡迎留言跟我分享,讓我可以學習更多。

看其他鐵蚊賽文章

Discussion