React Hook useState 与 this.setState 细节使用和差异
React Hook 一定会用到的属性是 useState
,对于 state 的设置和获取在 Hook 的体现就是 useState
基本用法不多重复:
const [count, setCount] = useState(0);
这里测试了几种使用情况,以及 hook function component 和 class component 的几种表现的区别。
场景1、先 setState 然后通过 setTimeout 取值
这个场景中,通过一个方法,每次先 setState
,然后通过 setTimeout
取 state,在 class component 的实现中,取了两个值,一个是方法内部作用域的值 count
,一个是 this.state.count
class component 的写法:
addHadnleTimeout2 是一个 click 事件处理方法
addHandleTimeout2 = () => {
const { count } = this.state;
console.log(`----timeout count ---- ${count}`)
this.setCount(count + 1);
setTimeout(() => {
console.log(`----this.state.count---- ${this.state.count}`);
console.log(`----count---- ${count}`);
}, 2000);
}
console 结果:
hook function component
const addHandleTimeout2 = () => {
console.log(`----timeout count ---- ${count}`)
setCount(count + 1);
setTimeout(() => {
console.log(`----count---- ${count}`);
}, 2000);
}
console 结果
区别点:
可以发现,当我在 setTimeout
中分别 console.log(count)
(function component) 和 console.log(this.state.count)
(class component) 的时候,两者输出的结果是不同的。
在 function component 中,setTimeout 的输出分别是:0、1、2、3
在 class component 中,setTimeout 每次输出分别是:4、4、4、4
从代码理解的角度来讲,两者行为应该是一致的,不一致的原因需要从两者角度去理解:
首先是对 class component 的解释:
- state 是 Immutable 的,setState 后一定会生成一个全新的 state 引用。
- 但 Class Component 通过 this.state 方式读取 state,这导致了每次代码执行都会拿到最新的 state 引用,所以快速点击4次的结果是 4 4 4 4。
然后是对 function component useState 的解释:
- useState 产生的数据也是 Immutable 的,通过数组第二个参数 Set 一个新值后,原来的值在下次渲染时会形成一个新的引用。
- 但由于对 state 的读取没有通过 this. 的方式,使得 每次 setTimeout 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。
为了理解这个渲染闭包环境,我们断点看下 function component 中这个方法每次取到的值:
第1次:
第2次:
第3次:
上面的三次断点中,每次这个方法中 count 都是方法一进来时拿到的 count,所以即使延迟 2S 去取 count 取到的也是上次的结果
相比于上面的结果,如果在 class component 中断点:
第1次:
第2次:
其他的以此类推
所以,其实在每次的渲染中,方法中拿到的 count 变量其实是上次的 state 的结果,但是 this.state.count
每次都会拿到最新的 state 的引用,所以挂载的属性 this.state.count
在 2s 之后拿到的最新的。
场景2、对象或数组一级属性 set 的表现
首先声明:下面的代码中,无论是 setArrayHandle
还是 setObjectHandle
都是一种不规范的写法,理论上我们都应该创建一个新的引用,而不是在之前的引用中直接修改属性
function component 中直接修改 state 属性,然后进行 set
const setArrayHandle = () => {
// const newList = Array.from(list);
const newList = list;
console.log(newList === list);
console.log(`---before--- ${list}`);
newList[0] = Math.floor(Math.random() * 10);
setList(newList);
}
const setObjectHandle = () => {
const newObj = obj;
// const newObj = Object.assign({}, obj);
console.log(newObj === obj);
newObj.name = Math.floor(Math.random() * 10);
setObj(newObj);
}
为了监听 state 的变化,使用了两个 useEffect
:
useEffect(() => {
console.log(`----effect--- ${JSON.stringify(obj)}`);
}, [obj]);
useEffect(() => {
console.log(`----effect--- ${JSON.stringify(list)}`);
}, [list]);
每次点击按钮的时候,都会触发其中的方法,然后直接修改了 state 的一个属性,这种用法其实是有问题的,虽然我把 obj
这个 state 赋值给了 newObj,但其实引用还是同一份。
这个时候,去 setObj(newObj)
会发现,其实页面根本没有触发重新渲染。
上面的图示中,左边是上面的代码示例触发的点击,可以发现,每次通过 console 输出的时候值都是变化的,但并没有触发模板的 re-render,effect 也没有触发。
而右边其实是我在 class component 直接修改了 this.state.obj.name=xxx
,然后去 setState,当然了 this.state.arr[0]=xxx
也是一样的修改逻辑
setArrayHandle = () => {
const newList = this.state.list;
console.log(newList === this.state.list);
console.log(`---before--- ${this.state.list}`);
this.state.list[0] = Math.floor(Math.random() * 10);
this.setState({
list: this.state.list
}, () => {
console.log(`---after--- ${this.state.list}`);
});
}
setObjectHandle = () => {
const newObj = this.state.obj;
console.log(newObj === this.state.obj);
console.log(`---before--- ${JSON.stringify(this.state.obj)}`);
this.state.obj.name = Math.floor(Math.random() * 10);
this.setState({ obj: this.state.obj });
console.log(`---after--- ${JSON.stringify(this.state.obj)}`);
}
再次声明,上面的写法是错误的
class component 这个不过多解释,这个没有什么疑义,关键是 function component,通过 useState 这个 hook 实现的方式上的表现
问题:是否可以认为,react hool setState 默认实现了依次类似于 PureComponent 的逻辑呢?
断点下来,可以发现走到了这里:
这里我们发现的是有一个对比,发现 eagerState
和 currentState
其实是相同的,因为本身我们就是修改的 state 的 obj.name,因此在这次闭包中,认为传过来的新的 state 其实和之前对比是相同的(之前的 state 是我们人工修改的值),这种情况下,就不会出发渲染
这个逻辑其实和 class component 中使用 PureComponent 差不多,使用 PureComponent 其实可以复现我们上面的情况
这里我们使用 PureComponent 代码实现的相同逻辑:
setObjectHandle = () => {
const newObj = this.state.obj;
console.log(newObj === this.state.obj);
console.log(`---before--- ${JSON.stringify(this.state.obj)}`);
this.state.obj.name = Math.floor(Math.random() * 10);
this.setState({ obj: this.state.obj });
console.log(`---after--- ${JSON.stringify(this.state.obj)}`);
}
效果如下:
其实我们可以发现,这个场景中我们也是直接修改了 this.state.obj.name = xxx
,在 setState 中,当我们把 this.state.obj
丢进去的时候,react 会将 之前的 this.state
和 传入的 this.state
进行浅比较,此时肯定是相等的,也不会触发重新渲染。
通过下面三个断点可以发现,这种场景下,setState 的时候,内存中 this.state.obj 和目标更新的 this.state.obj 浅比较结果其实是一样的。
因此不会触发更新
代码
相关代码可以在:https://github.com/postbird/react-hook-practice 这个仓库中找到
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://www.ptbird.cn/react-hook-usestate-setState.html
转载请注明文章原始出处 !