使用 JS 及 React Hook 時須要注意過期閉包的坑(文中有解決方法)

做者:Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin

上個月本身花了 1300 買了阿里的服務器來學習 node 及對應的框架,在 11 號以前它們有作活動,1300 的配置如今一年只要 86 元,三年只要229元,真心以爲很划算了,能夠點擊下面連接進行參與:javascript

https://www.aliyun.com/1111/2...html


爲了保證的可讀性,本文采用意譯而非直譯。前端

1. JS 中的閉包

下面定義了一個工廠函數 createIncrement(i),它返回一個increment函數。以後,每次調用increment函數時,內部計數器的值都會增長ijava

function createIncrement(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
  }
  return increment;
}

const inc = createIncrement(1);
inc(); // 1
inc(); // 2

createIncrement(1) 返回一個增量函數,該函數賦值給inc變量。當調用inc()時,value 變量加1node

第一次調用inc()返回1,第二次調用返回2,依此類推。react

這挺趣的,只要調用inc()還不帶參數,JS 仍然知道當前 valuei 的增量,來看看這玩意是如何工做的。git

原理就在 createIncrement() 中。當在函數上返回一個函數時,有會有閉包產生。閉包捕獲詞法做用域中的變量 valueigithub

詞法做用域是定義閉包的外部做用域。在本例中,increment() 的詞法做用域是createIncrement()的做用域,其中包含變量 valueinpm

clipboard.png

不管在何處調用 inc(),甚至在 createIncrement() 的做用域以外,它均可以訪問 valueisegmentfault

閉包是一個能夠從其詞法做用域記住和修改變量的函數,無論執行做用域是什麼。

繼續這個例子,能夠在任何地方調用 inc(),甚至在異步回調中也能夠:

(function() {
  inc(); // 3
}());

setTimeout(function() {
  inc(); // 4
}, 1000);

2. React Hooks 中的閉包

經過簡化狀態重用和反作用管理,Hooks 取代了基於類的組件。此外,我們能夠將重複的邏輯提取到自定義 Hook 中,以便在應用程序之間重用。

Hooks 嚴重依賴於 JS 閉包,可是閉包有時很棘手。

當我們使用一個有多種反作用和狀態管理的 React 組件時,可能會遇到的一個問題是過期的閉包,這可能很難解決。

我們從提煉出過期的閉包開始。而後,看看過期的閉包如何影響 React Hook,以及如何解決這個問題。

3. 過期的閉包

工廠函數createIncrement(i)返回一個increment函數。increment 函數對 value 增長i請輸入代碼 ,並返回一個記錄當前 value 的函數

function createIncrement(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    const message = `Current value is ${value}`;
    return function logValue() {
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrement(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 沒法正確工做
log();             // 打印 "Current value is 1"

在第一次調用inc()時,返回的閉包被分配給變量 log。對 inc()3 次調用的增量 value3

最後,調用log() 打印 message 「Current value is 1」,這是出乎意料的,由於此時 value 等於 3

log()是過期的閉包。在第一次調用 inc() 時,閉包 log() 捕獲了具備 「Current value is 1」message 變量。而如今,當 value 已是 3 時,message 變量已通過時了。

過期的閉包捕獲具備過期值的變量。

4.修復過期閉包的問題

使用新的閉包

解決過期閉包的第一種方法是找到捕獲最新變量的閉包。

我們找到捕獲了最新 message 變量的閉包。就是從最後一次調用 inc() 返回的閉包。

const inc = createIncrement(1);

inc();  // 打印 1
inc();  // 打印 2
const latestLog = inc(); // 打印 3
// 正常工做
latestLog(); // 打印 "Current value is 3"

latestLog 捕獲的 message 變量具備最新的的值 「Current value is 3」。

順便說一下,這大概就是 React Hook 處理閉包新鮮度的方式。

Hooks 實現假設在組件從新渲染之間,做爲 Hook 回調提供的最新閉包(例如 useEffect(callback)) 已經從組件的函數做用域捕獲了最新的變量。

關閉已更改的變量

第二種方法是讓logValue()直接使用 value

讓咱們移動行 const message = ...;logValue() 函數體中:

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 正常工做
log();             // 打印 "Current value is 3"

logValue() 關閉 createIncrementFixed() 做用域內的 value 變量。log() 如今打印正確的消息「Current value is 3」。

5. Hook 中過期的閉包

useEffect()

如今來研究一下在使用 useEffect() Hook 時出現過期閉包的常見狀況。

在組件 <WatchCount> 中,useEffect()每秒打印 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) }>
        加1
      </button>
    </div>
  );
}

打開 CodeSandbox 並單擊幾回加1按鈕。而後看看控制檯,每2秒打印 Count is: 0

咋這樣呢?

在第一次渲染時,log() 中閉包捕獲 count 變量的值 0。事後,即便 count 增長,log()中使用的仍然是初始化的值 0log() 中的閉包是一個過期的閉包。

解決方案是讓 useEffect()知道 log() 中的閉包依賴於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() 就更新閉包。

一樣打開修復的 codesandbox,單擊幾回加1按鈕。而後看看控制檯,此次打印就是正確的值了。

正確管理 Hook 依賴關係是解決過期閉包問題的關鍵。推薦安裝 eslint-plugin-react-hooks,它能夠幫助我們檢測被遺忘的依賴項。

useState()

組件<DelayedCount>有 2 個按鈕:

  • 點擊按鍵 「Increase async」 在異步模式下以1秒的延遲遞增計數器
  • 在同步模式下,點擊按鍵 「Increase sync」 會當即增長計數器。

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

    function handleClickAsync() {

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

    }

    function handleClickSync() {

    setCount(count + 1);

    }

    return (

    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>

    );
    }

如今打開 codesandbox 演示。點擊 「Increase async」 按鍵而後當即點擊 「Increase sync」 按鈕,count 只更新到 1

這是由於 delay() 是一個過期的閉包。

來看看這個過程發生了什麼:

  1. 初始渲染:count 值爲 0
  2. 點擊 'Increase async' 按鈕。delay() 閉包捕獲 count 的值 0setTimeout() 1 秒後調用 delay()
  3. 點擊 「Increase async」 按鍵。handleClickSync() 調用 setCount(0 + 1)count 的值設置爲 1,組件從新渲染。
  4. 1 秒以後,setTimeout() 執行 delay() 函數。可是 delay() 中閉包保存 count 的值是初始渲染的值 0,因此調用 setState(0 + 1),結果count保持爲 1

delay() 是一個過期的閉包,它使用在初始渲染期間捕獲的過期的 count 變量。

爲了解決這個問題,可使用函數方法來更新 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>
  );
}

如今 setCount(count => count + 1) 更新了 delay() 中的 count 狀態。React 確保將最新狀態值做爲參數提供給更新狀態函數,過期的閉包的問題就解決了。

總結

閉包是一個函數,它從定義變量的地方(或其詞法範圍)捕獲變量。閉包是每一個 JS 開發人員都應該知道的一個重要概念。

當閉包捕獲過期的變量時,就會出現過期閉包的問題。解決過期閉包的一個有效方法是正確設置 React Hook 的依賴項。或者,對於過期的狀態,使用函數方式更新狀態。

你認爲閉包使得 React Hook 很難理解嗎?

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

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

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

阿里雲最近在作活動,低至2折,有興趣能夠看看:https://promotion.aliyun.com/...

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq449245884/xiaozhi

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索