close
本文分享一個短小而又深刻的 React Hook 場景題,這個例子涉及到:
hook 閉包問題
state 更新機制

希望看完以後你會對 React 函數組件有更深入的了解。

場景復現

整個 Demo 非常簡單,大家可以自己在電腦上嘗試一下。

首先,有一個 button 和一個 list:

<divclassName="App"><buttononClick={add}>Add</button>{list.map(val=>val)}</div>

list 是使用 useState 管理的狀態。button 綁定了事件 onClick={add}。

點擊按鈕,會執行 add 方法向 list 中加入一些內容。

exportdefaultfunctionApp(){const[list,setList]=useState([]);constadd=()=>{//...};return(<divclassName="App"><buttononClick={add}>Add</button>{list.map(val=>val)}</div>);}

現在頁面看起來像這樣:

我們繼續,先在 App 外部定義變量 i。

leti=0;exportdefaultfunctionApp(){//...}

接着重點來看看 add 方法。

調用 add,會向 list 中添加新的 button,新 button 也綁定了 onClick={add}。

constadd=()=>{setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));};

當我們點擊「Add 按鈕」7 次,會展示:

在線示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js

問題

現在問題來了:現在我們點擊這些「數字按鈕」,頁面會怎麼展示呢

比如點擊 0,頁面會如何展示,list 最終結果是什麼
點擊 6,又會如何展示

你可以先停下來思考一下,再繼續往下讀。

解答

有的同學可能會認為,點擊「數字按鈕」後,會有新的 button 被添加到 list 中。

先說結論,這個答案並不正確。

真正的現象是,點擊數字按鈕後:

列表的長度將會變成 點擊的數字 + 1
並且列表最後一個數字會變成 點擊之前最大的數字 + 1。

文字不太容易理解,舉一個🌰。

假設當前列表為:

我們點擊 0:

列表的長度會變成 0 + 1 = 1
列表最後一個數字會變成 6 + 1 = 7

如果點擊 2:

列表長度會變成 2 + 1 = 3
列表最後一個數字會變次 6 + 1 = 7

為什麼會這樣呢?

原理剖析

造成這種反直覺現象的原因有兩個:

hook 閉包問題
state 更新機制

再來看看點擊按鈕會調用的 add 函數:

constadd=()=>{setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));};

當執行 add 函數時,由於訪問了外層函數 App 內的變量,所以會根據 App 函數上下文形成閉包,閉包內包括:

add 函數
list 變量
setList 方法

list 和 setList 是調用 useState() 返回的。

這裡通常有一個誤解:多次調用 useState,返回的 list 都是同一個對象。

實際上,useState 返回的 list 都是基於 base state 計算出來的:

currentstate=basestate+update1+update2+…

每次會將上一次的 prev state 與 update 進行合併得到新的 current state。

因此,每次調用 useState 返回的 list 都不是同一個對象,它們的內存地址不同。

這會導致每個「數字按鈕」的 add 函數處於不同的閉包中,每個閉包當中的 list 都不同。

而變量 i 是聲明在 App 外層的模塊級變量,在每個閉包中 i 都是相同的。

leti=0;exportdefaultfunctionApp(){//...}

所以,在點擊 0 時:

i 是模塊級變量,值為 6
list 是閉包中的變量,值為 []

add 函數實際上執行的是:

setList([].concat(<buttonkey={7}onClick={add}>{7}</button>));

所以 list 最終變成了 [7]。

當點擊 2 時:

i 是模塊級變量,值為 6
list 是閉包中的變量,值為 [0,1]

add 函數實際上執行的是:

setList([0,1].concat(<buttonkey={7}onClick={add}>{7}</button>));

所以 list 最終變成了 [0, 1, 7]。

為了方便理解,這裡的 [0, 1, 7] 省略了外層的 <button> 標籤

如何解決

那麼如何解決這個閉包問題,在 list 後面正常拼接 button 呢?

很簡單,只要將 list 從閉包中清理出去就可以了,將 setList 參數改為函數形式。

之前是:

setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));

修改為:

setList(list=>list.concat(<buttonkey={i}onClick={add}>{i++}</button>));

這樣,我們點擊「Add 按鈕」或任意「數字按鈕」都會正常在 list 後面拼接新按鈕。

大家可以通過在線示例來加深理解。

在線示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js

總結

由於 state 的更新機制是:

currentstate=basestate+update1+update2+…

所以每次調用 useState,返回值 list 都是不同的對象。

並且由於閉包的存在,每個「數字按鈕」add 函數中的 list 都不同。

兩者共同作用,造成了這種不符合直覺的現象。

如何解決這種閉包問題呢?我們可以將 setState 改為函數形式,將變量從閉包中清理出去。

參考
https://betterprogramming.pub/a-react-hooks-challenge-for-senior-react-developers-f1190e1939ec
https://github.com/facebook/react/blob/a8c9cb18b7/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js
謝謝支持❤️

如果覺得有用,就點讚、在看、分享吧,謝謝大家啦~


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()