什麼是過時閉包及如何解決過時閉包的坑

前端小智發表於2019-11-04

作者:Dmitri Pavlutin

譯者:前端小智

來源:dmitripavlutin


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

1. JS 中的閉包

下面定義了一個工廠函式 createIncrement(i),它返回一個increment函式。之後,每次呼叫increment函式時,內部計數器的值都會增加i

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 變數加1

第一次呼叫inc()返回1,第二次呼叫返回2,依此類推。

這挺趣的,只要呼叫inc()還不帶引數,JS 仍然知道當前 valuei 的增量,來看看這玩意是如何工作的。

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

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

什麼是過時閉包及如何解決過時閉包的坑

無論在何處呼叫 inc(),甚至在 createIncrement() 的作用域之外,它都可以訪問 valuei

閉包是一個可以從其詞法作用域記住和修改變數的函式,不管執行作用域是什麼。

繼續這個例子,可以在任何地方呼叫 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

原文: dmitripavlutin.com/simple-expl… dmitripavlutin.com/react-hooks…

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

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。

clipboard.png

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

什麼是過時閉包及如何解決過時閉包的坑

相關文章