天天看點

使用 React Hooks 時需要注意過時的閉包!

釋出于 2 月 25 日

作者:Shadeed

譯者:前端小智

來源:dmitripavlutin

點贊再看,微信搜尋【大遷世界】,B站關注【前端小智】這個沒有大廠背景,但有着一股向上積極心态人。本文

GitHub

https://github.com/qq44924588... 上已經收錄,文章的已分類,也整理了很多我的檔案,和教程資料。

最近開源了一個 Vue 元件,還不夠完善,歡迎大家來一起完善它,也希望大家能給個 star 支援一下,謝謝各位了。

github 位址:https://github.com/qq44924588...

Hooks 簡化了 React 元件内部狀态和副作用的管理。 此外,可以将重複的邏輯提取到自定義 Hooks 中,以在整個應用程式中重複使用。

Hooks 嚴重依賴于 JS 閉包。這就是為什麼 Hooks 如此具有表現力和簡單,但是閉包有時很棘手。

使用 Hooks 時可能遇到的一個問題就是過時的閉包,這可能很難解決。

讓我們從過時的裝飾開始。 然後,看看到過時的閉包如何影響 React Hooks,以及如何解決該問題。

1.過時的閉包

工廠函數

createIncrement(incBy)

傳回一個

increment

log

函數的元組。 調用時,

increment()

函數将内部

value

增加

incBy

,而

log()

僅列印一條消息,其中包含有關目前

value

的資訊:

function createIncrement(incBy) {
  let value = 0;

  function increment() {
    value += incBy;
    console.log(value);
  }

 const message = `Current value is ${value}`; function log() { console.log(message); }  
  return [increment, log];
}

const [increment, log] = createIncrement(1);
increment(); //  1
increment(); //  2
increment(); //  3
// 不能正确工作!
log();       //  "Current value is 0"           

複制

[increment, log] = createIncrement(1)

傳回一個函數元組:一個函數增加内部值,另一個函數記錄目前值。

然後,

increment()

的3次調用将

value

遞增到3。

最後,

log()

調用列印消息是

Current value is 0

,這有點出乎意料的,因為此時

value

3

了。

log()

是一個過時的閉包。閉包

log()

捕獲了值為

"Current value is 0"

message

變量。

即使

value

變量在調用

increment()

時被增加多次,

message

變量也不會更新,并且總是保持一個過時的值

"Current value is 0"

過時的閉包捕獲具有過時值的變量。

2.修複過時的閉包

修複過時的

log()

問題需要關閉實際更改的變量:

value

的閉包。

我們将語句

const message = ...;

移動到

log()

函數内部:

function createIncrement(incBy) {
  let value = 0;

  function increment() {
    value += incBy;
    console.log(value);
  }

  function log() {
 const message = `Current value is ${value}`;    console.log(message);
  }
  
  return [increment, log];
}

const [increment, log] = createIncrement(1);
increment(); //  1
increment(); //  2
increment(); //  3
// Works!
log();       // "Current value is 3"           

複制

現在,在調用了 3 次

increment()

函數之後,調用

log()

記錄了實際

value

"Current value is 3"

3. Hooks 中的過時閉包

3.1 useEffect()

我們來看一下使用

useEffect()

過時閉包的常見情況。

在元件

<WatchCount>

中,

useEffect()

中每2秒記錄一次

count

的值

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

  useEffect(function() {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);

  return (
    <div> {count} <button onClick={() => setCount(count + 1) }> Increase </button> </div>
  );
}           

複制

打開事例(https://codesandbox.io/s/stale-closure-use-effect-broken-2-gyhzk)

并點選幾次增加按鈕。然後看看控制台,每2秒出現一次

Count is: 0

,盡管

count

狀态變量實際上已經增加了幾次。

為什麼會這樣?

第一次渲染時,狀态變量

count

初始化為

元件安裝後,

useEffect()

調用

setInterval(log, 2000)

計時器函數,該計時器函數計劃每2秒調用一次

log()

函數。 在這裡,閉包

log()

捕獲到

count

變量為

之後,即使在單擊

Increase

按鈕時

count

增加,計時器函數每2秒調用一次的

log()

,使用

count

的值仍然是

log()

成為一個過時的閉包。

解決方案是讓

useEffect()

知道閉包

log()

依賴于

count

,并在

count

改變時正确處理間隔的重置

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

  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
 }, [count]);
  return (
    <div>
 {count}
 <button onClick={() => setCount(count + 1) }>
 Increase
 </button>
 </div>
  );
}           

複制

正确設定依賴項後,一旦

count

發生變化,

useEffect()

就會更新閉包。

3.2 useState()

<DelayedCount>

元件有1個

button

,以1秒延遲異步增加計數器。

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

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 1000);
  }

  return (
    <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div>
  );
}           

複制

現在打開示範(https://codesandbox.io/s/use-...。 快速單擊2次按鈕。 計數器僅更新為

1

,而不是預期的

2

每次單擊

setTimeout(delay, 1000)

将在1秒後執行

delay()

delay()

此時捕獲到的

count

兩個

delay()

都将狀态更新為相同的值

:setCount(count + 1) = setCount(0 + 1) = setCount(1)

這是因為第二次單擊的

delay()

閉包中已捕獲了過時的

count

變量為

為了解決這個問題,我們使用函數式方法s

etCount(count => count + 1)

來更新

count

狀态

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

  function handleClickAsync() {
    setTimeout(function delay() {
 setCount(count => count + 1);    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
 {count}
 <button onClick={handleClickAsync}>Increase async</button>
 <button onClick={handleClickSync}>Increase sync</button>
 </div>
  );
}           

複制

打開示範(https://codesandbox.io/s/use-...。 再次快速單擊按鈕

2

次。 計數器顯示正确的值

2

當一個傳回基于前一個狀态的新狀态的回調函數被提供給狀态更新函數時,React確定将最新的狀态值作為該回調函數的參數提供

setCount(alwaysActualStateValue => newStateValue);           

複制

這就是為什麼在狀态更新過程中出現的過時裝飾問題可以通過函數這種方式來解決。

4.總結

當閉包捕獲過時的變量時,就會發生過時的閉包問題。

解決過時閉包的有效方法是正确設定React鈎子的依賴項。或者,在失效狀态的情況下,使用函數方式更新狀态。

~完,我是小智,我要去刷碗了。

代碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。

原文:https://dmitripavlutin.com/re...