React Hooks 中的閉包問題

前言

今天中午在領完盒飯,吃飯的時候,正吃着深海鱈魚片,蘸上番茄醬,那美味,簡直無以言表。忽然產品急匆匆的跑過來講:「今天需求能上線吧?」我突然虎軀一震,想到本身遇到個問題遲遲找不到緣由,怯怯的回答道:「能...能吧...」,產品聽到‘能’這個字便哼着小曲揚長而去,留下我獨自一人,面對着已經變味的深海鱈魚片...一遍又一遍的想着問題該如何解決...react

1、從JS中的閉包提及

JS的閉包本質上源自兩點,詞法做用域和函數當前值傳遞。npm

閉包的造成很簡單,就是在函數執行完畢後,返回函數,或者將函數得以保留下來,即造成閉包。bash

關於詞法做用域相關的知識點,能夠查閱《你不知道的JavaScript》找到答案。閉包

React Hooks中的閉包和咱們在JS中見到的閉包並沒有不一樣。異步

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

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
    }
    return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20
複製代碼

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

第一次調用inc()返回10,第二次調用返回20,依此類推。ui

調用inc()時不帶參數,JS仍然能夠獲取到當前 valuei的增量,來看看它是如何工做的。spa

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

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

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

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

2、React Hooks中的閉包

經過簡化狀態重用和反作用管理,Hooks 取代了基於類的組件。此外,我們能夠將重複的邏輯提取到自定義 Hook 中,以便在應用程序之間重用。Hooks嚴重依賴於 JS 閉包,可是閉包有時很棘手。

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

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() { // setState至關於logValue函數
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(10)
const log = inc() // 10,將當前的value值固定
inc() // 20
inc() // 30

log() // "Current value is 10" 未能正確打印30
複製代碼
function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState至關於logValue函數
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(1) // i被固定爲1,輸入幾就被固定爲幾
inc() // 1
const log = inc() // 2
inc() // 3

log() // "Current value is 2" 未能正確打印3
複製代碼

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

4、修復過期閉包的問題

(1) 使用新的閉包

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

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

const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"
複製代碼

以上就是React Hook處理閉包新鮮度的方法了。

Hooks實現假設在組件從新渲染以前,最爲Hook回調提供的最新閉包(例如useEffect(callback))已經從組件的函數做用域捕獲了最新的變量。也就是說在useEffect的第二個參數[]加入監聽變化的值,在每次變化時,執行function,獲取最新的閉包。

(2) 關閉已更改的變量

第二種方法是讓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()如今打印正確的消息。

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>
    )
}
複製代碼

點擊幾回加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]); // 看這裏,這行是重點,count變化後從新渲染useEffect

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

設置依賴項後,一旦count更改,useEffect()就更新閉包。

正確管理 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>
  )
}
複製代碼

點擊 「Increase async」 按鍵而後當即點擊 「Increase sync」 按鈕,count只更新到1

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

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

初始渲染:count 值爲 0。 點擊 'Increase async' 按鈕。delay()閉包捕獲 count 的值 0setTimeout() 1 秒後調用 delay()。 點擊 「Increase async」 按鍵。handleClickSync() 調用 setCount(0 + 1)count的值設置爲 1,組件從新渲染。 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 確保將最新狀態值做爲參數提供給更新狀態函數,過期的閉包的問題就解決了。

總結

閉包是一個函數,它從定義變量的地方(或其詞法範圍)捕獲變量。

當閉包捕獲過期的變量時,就會出現過期閉包的問題。

解決閉包的有效方法

  1. 正確設置React Hook 的依賴項
  2. 對於過期的狀態,使用函數方式更新狀態
相關文章
相關標籤/搜索