hooks的故事(2):闭包陷阱
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
返回的就是传入的第一个对象。总儿言之,就是在设置的时候返回了同一个对象。