淺談react useEffect閉包的坑
問題代碼
看一段因為useEffect導致的閉包問題代碼
const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('click', clickHandle) } }, []); const inputHandle = e => { setV(e.target.value) } return ( <> <input value={v} onChange={inputHandle} /> <button ref={btn} >測試</button> </> )
useEffect的依賴項數組為空,所以在頁面渲染完成之後,內部代碼隻會執行一次,頁面銷毀再執行一次。此時在輸入框中輸入任意字符,再點擊測試按鈕,得到的輸出為空,之後無論如何輸入任何字符,再點擊測試按鈕時,輸出的結果仍為空。
為什麼會這樣呢?其實就是閉包所造成的。
產生原因
函數的作用域在函數定義的時候就決定瞭
給btn註冊點擊事件時,作用域如下:
能訪問到的自由變量v此時還是空值。當點擊事件觸發時,執行點擊回調函數,此時先創建執行上下文,會拷貝作用域鏈到執行上下文中。
- 如果未在輸入框內輸入字符,此時點擊拿到的v還是原來那個v
- 如果在輸入框內輸入瞭字符,此時調用瞭setV修改瞭state,頁面觸發render,組件內部代碼會重新執行一遍,重新聲明瞭一個v,v就不再是原來那個v,這裡點擊事件裡作用域中的v還是舊的v,這是兩個不同的v
產生場景
- 事件綁定。比如示例代碼中,在頁面最初渲染完成後隻綁定一次事件的情況,比如使用echarts,在useEffect中獲取echarts的實例並綁定事件
- 定時器。頁面加載後註冊一個定時器,定時器內的函數也會產生如此的閉包問題。
解決辦法
針對這個閉包問題下面大致給出5種解決辦法
1. 以賦值方式直接修改v,並將修改v的方法用useCallback包裹起來
將修改v的方法用useCallback包裹起來,被useCallback包裹的函數將被緩存,由於依賴項的數組為空,所以這裡直接賦值的方式修改的v是舊的v,此種方法不推薦,因為setState才是官方推薦的修改state的方式,這裡仍然使用setV隻是為瞭觸發rerender
// v 的聲明 由 const 改為 var,方便直接修改 var [v, setV] = useState(''); const inputHandle = useCallback(e => { let { value } = e.target v = value setV(value) }, [])
2. 給useEffect的依賴項加上v
這也許是大多數人首先想到的辦法,既然v是舊的,那麼每次v更新的時候,重新註冊一次事件不就行瞭,但是這樣的會導致每次v更新都得重新註冊,理論應該隻需要註冊一次的事件變成瞭多次。
3. 避免v被重新聲明
以let或var的方式聲明某個變量代替v,直接修改這個變量,而不是要setState相關函數觸發render,這樣就不會被重新聲明,點擊的回調函數裡就能拿到“最新”的值,但這個方法更不推薦,就此示例來說,input組件由於沒有rerender而至始至終都是顯示空值,不符合操作預期。
4. 使用useRef代替useState
const btn = useRef(); const vRef = useRef(''); const [v, setV] = useStat(''); useEffect(() => { let clickHandle = () => { console.log('v:', vRef.current); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('click', clickHandle) } }, []); const inputHandle = e => { let { value } = e.target vRef.current = value setV(value) } return ( <> <input value={v} onChange={inputHandle} /> <button ref={btn} >測試</button> </> )
useRef的方案之所以有效,是因為每次input的change修改的是vRef這個對象的current屬性,而vRef始終是那個vRef,即使rerender,由於vRef是對象,所以變量存儲在棧內存中的值是該對象在堆內存中的地址,隻是一個引用,隻修改對象的某個屬性,該引用並不會改變。所以點擊事件中的作用域鏈始終訪問的都是同一個vRef
5. 將v換成對象類型
其實和使用useRef一樣,隻要是對象,僅修改某個屬性也不會改變該state所指向的地址。
代碼地址
點這裡看測試代碼
到此這篇關於淺談react useEffect閉包的坑的文章就介紹到這瞭,更多相關react useEffect閉包內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 30分鐘帶你全面瞭解React Hooks
- 如何解決React useEffect鉤子帶來的無限循環問題
- React Hook用法示例詳解(6個常見hook)
- React中10種Hook的使用介紹
- React hooks useState異步問題及解決