hooks 的故事(1):閉包陷阱
經典的場景:
function App(){
const [count, setCount] = useState(1);
useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 1000)
}, [])
}
不管你如何setCount,輸出的count始終是1!
經典的閉包場景
for ( var i=0; i<5; i++ ) {
setTimeout(()=>{
console.log(i)
}, 0)
}//5,5,5,5,5
// 正確的版本
for ( var i=0; i<5; i++ ) {
(function(i){
setTimeout(()=>{
console.log(i)
}, 0)
})(i)
}// 0,1,2,3,4
這是一道經典的js題,輸出是5個5,而非 0,1,2,3,4
原因是因為settimeout被放入任務隊列,拿出執行時取到的i就是5
function App(){
const [count, setCount] = useState(1);
useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 1000)
},
[])
}
graph LR a[初次渲染]-->b[執行App]-->c[usestate設置count為初始1]-->d[useeffect 設置定時器每隔一秒打印count]
graph LR a[state改變]-->b[試圖更新]-->c[按照fiber鏈表執行hooks]-->d[useEffect deps 不變]
deps不變,就不會重新執行。而一個匿名函數引用了一開始的count(count==1).
這也就是典型的閉包陷阱。變量因為在匿名回調函數引用,形成了一個閉包一直被保存
解決辦法
function App(){
const [count, setCount] = useState(1);
useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 1000)
},
[count])
}
根據我們上面的推測,最直觀的解決就是把狀態直接放到deps數組。
回到我們關於閉包的討論。閉包本質上就是執行現場的保存。所以需要保持函數執行的正確性。關鍵在回調函數執行的時機
function App() {
return <Demo1 />
}
function Demo1(){
const [num1, setNum1] = useState(1)
const [num2, setNum2] = useState(10)
const text = useMemo(()=>{
return `num1: ${num1} | num2:${num2}`
}, [num2])
function handClick(){
setNum1(2)
setNum2(20)
}
return (
<div>
{text}
<div><button onClick={handClick}>click!</button></div>
</div>
)
}
比如上面這個例子,我們並沒有在useMemo的deps中寫入num1,但執行之后你會發現點擊按鈕之后兩個量都會變!因為回調函數在正確的時機被rerun
為什么useRef每次都可以拿到新鮮的值
一句話,useRef返回的是同一個對象,指向同一片內存
/* 將這些相關的變量寫在函數外 以模擬react hooks對應的對象 */
let isC = false
let isInit = true; // 模擬組件第一次加載
let ref = {
current: null
}
function useEffect(cb){
// 這里用來模擬 useEffect 依賴為 [] 的時候只執行一次。
if (isC) return
isC = true
cb()
}
function useRef(value){
// 組件是第一次加載的話設置值 否則直接返回對象
if ( isInit ) {
ref.current = value
isInit = false
}
return ref
}
function App(){
let ref_ = useRef(1)
ref_.current++
useEffect(()=>{
setInterval(()=>{
console.log(ref.current) // 3
}, 2000)
})
}
// 連續執行兩次 第一次組件加載 第二次組件更新
App()
App()
所以,提出一個合理的設想。只要我們能保證每次組件更新的時候,useState
返回的是同一個對象的話?我們也能繞開閉包陷阱這個情景嗎? 試一下吧。
function App() {
// return <Demo1 />
return <Demo2 />
}
function Demo2(){
const [obj, setObj] = useState({name: 'chechengyi'})
useEffect(()=>{
setInterval(()=>{
console.log(obj)
}, 2000)
}, [])
function handClick(){
setObj((prevState)=> {
var nowObj = Object.assign(prevState, {
name: 'baobao',
age: 24
})
console.log(nowObj == prevState)
return nowObj
})
}
return (
<div>
<div>
<span>name: {obj.name} | age: {obj.age}</span>
<div><button onClick={handClick}>click!</button></div>
</div>
</div>
)
}
Object.assign
返回的就是傳入的第一個對象。總兒言之,就是在設置的時候返回了同一個對象。