React setState同步异步问题
在函数组件中,我们最经常用到的就是useState
、useEffect
。 今天在函数组件中使用useState发现了一些问题,setState的更新是异步的,我们先来看看class组件setState异步问题。
1.class组件中setState()异步问题
import React, { PureComponent } from 'react';
class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
title: 'Hello Alice'
}
}
changText() {
this.setState({
title: '你好呀,李银河'
})
console.log(this.state.title); // Hello Alice
}
render() {
return (
<div>
<h2>{this.state.title}</h2>
<button onClick={e => {this.changText()}}>改变文本</button>
</div>
);
}
}
export default App;
我们发现在changText()函数中,我们虽然已经setState给title一个新的值,但是在console.log()的时候打印的依然是原来state中的值。
可见setState是异步的,我们并不能在setState后马上拿到最新的结果
2.为什么要将setState设计为异步呢?
相信大家,刚接触setState的时候一样都会有这样的疑问,直接同步更新不就好了吗?
- 然后我去找了很多资料都写的不是很清楚,最后在github上看到有人在讨论这个问题,而且这个问题
- React核心成员(Redux的作者)Dan Abramov也有对应的回复,他在里面写了很长的篇幅来解释
- github.com/facebook/re…
从Dan Abramov的回复中可以归纳出几点
setState
设计为异步,可以显著的提升性能;-
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样同步更新的效率是很低;
- 如果我们知道可能会得到多个更新,最好批量更新。
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
-
- state和props不能保持一致性,会在开发中产生很多的问题;
那么在class组件中如何可以获取到更新后的值呢?
- setState()接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
- 格式如下:
setState(partialState, callback)
我们将上面的代码修改一下,可以看到通过回调函数已经可以得到最新的结果了
changeText() {
this.setState({
message: "你好啊,李银河"
}, () => {
console.log(this.state.message); // 你好啊,李银河
});
}
当然,我们也可以在生命周期函数实现:
componentDidUpdate(prevProps, provState, snapshot) {
console.log(this.state.message);
}
3.setState一定是异步更新的吗?
其实不然,我们可以做下面两个测试
1.使用setTimeOut
changeText() {
setTimeout(() => {
this.setState({
message: "你好啊,李银河"
});
console.log(this.state.message); // 你好啊,李银河
}, 0);
}
2.使用原生DOM事件
componentDidMount() {
const btnEl = document.getElementById("btn");
btnEl.addEventListener('click', () => {
this.setState({
message: "你好啊,李银河"
});
console.log(this.state.message); // 你好啊,李银河
})
}
可以分成两种情况:
- 在组件生命周期或React合成事件中,setState是异步;
- 在setTimeout或者原生dom事件中,setState是同步;
4.函数式组件通过hook,setState()异步问题
我们来看,我今天做的案例
import React, { useState, useEffect } from 'react';
import { layoutEmitter } from '@/utils/EventEmitter';
interface ItemProps {
initNumber: number;
}
export default ({ initNumber = 0 }: ItemProps) => {
const [state, setState] = useState(initNumber);
return <button
style={{ fontSize: '30px' }}
onClick={() => {
setState(state => state + 1)
layoutEmitter.emit({ state });
}}
>EventEmitter {state} </button>
};
上述代码中,在button的onClick
事件中setState
,然后把state传给layoutEmitter.emit({ state });
但是都得不到前面setState
后的最新值,也是因setState
是异步更新问题造成的,useState
中的setState
只有一个参数,并没有像class组件中的setState
第二个参数是一个回调函数,可以在回调函数中实现同步更新
5.函数式组件实现同步更新
那我们要怎么实现同步更新呢?难道也是在setState(),的第二个参数中传入一个回调函数?
试了一下并不可以,而且报了一个警告
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument.
既然useState这个 Hook行不通,我们就换一个试试,既然都在说React现在全面拥抱Hook那肯定有它强大的地方
6.通过useEffect实现同步更新数据
再次的查找github中有没有相关的我问题,最后在一个讨论里也看到了Dan Abramov的回复,也建议我们通过useEffect
来实现
我们之前学过useEffect
,它的第一个参数是一个回调函数
,在默认情况下,每次更新都会执行这个回调函数
那我们就可以在按钮发生点击时setState() 然后在userEffect中实现传递参数,这样我们拿到的数据一定是最新的,那么我们动手尝试一下吧
import React, { useState, useEffect } from 'react';
import { layoutEmitter } from '@/utils/EventEmitter';
interface ItemProps {
initNumber: number;
}
export default ({ initNumber = 0 }: ItemProps) => {
const [state, setState] = useState(initNumber);
useEffect(() => {
layoutEmitter.emit({ state });
}, [state])
return <button
style={{ fontSize: '30px' }}
onClick={() => {
setState(state => state + 1)
}}
>EventEmitter {state} </button>
};
代码写完,我们运行看看发现出错了
PLAINTEXT
Type error: _this.subscriptions not a function
我们查看EventEmitter.ts
和index.ts
发现代码执行的顺序是是在index.ts
中调用useSubscription
后将this.subscriptions = subscription;
然后emit再通过this来调用
type Subscription<T> = (val: T) => void;
class EventEmitter<T> {
private subscriptions: { (arg0: T): void; (val: T): void; } | undefined;
emit = (val: T) => {
//@ts-ignore
this.subscriptions(val);
};
useSubscription = (callback: Subscription<T>) => {
function subscription(val: T) {
if (callback) {
callback(val);
}
}
this.subscriptions = subscription;
};
}
const layoutEmitter = new EventEmitter();
export { layoutEmitter };
而我们的index.ts
使用了这个组件这就造成了,在index挂载到DOM上也会去执行下的useEffect
,这样就造成了按钮还没点击,就已经掉调用了一次layoutEmitter.emit({ state });
而这时候this.subscriptions = subscription;
还并没有执行到
export default () => {
const [list, setList] = useState([]);
useEffect(() => {
layoutEmitter.useSubscription((data) => {
list.push(data);
console.log(data);
console.log(list);
const listData = [...list]
setList(listData);
});
}, [])
return (
<div>
<EventEmitterButton initNumber={11} />
<p>list length:{list.length}</p>
{
list.map((item: ItemProps) => <p key={item.state}>{item.state}</p>)
}
</div>
)
};
那我们要如何去解决这个问题呢?
useEffect
的第二个参数 DependencyList
是一个数组,可以帮助我们解决这个问题。
这个参数的作用是该useEffect
在哪些state发生变化时,才重新执行;(受谁的影响)
我们来看看案例:
//1.第二个参数不填,在源码中deps?: DependencyList是一个可以选参数,我们不写也没关系
useEffect(()=>{
console.log(props.number)
setNumber(props.number)
}) //所有更新都执行
//2.一个空的数组
useEffect(()=>{
console.log(props)
},[]) //仅在挂载和卸载的时候执行
//3.定义为state
const [state,setState] = useState(0)
useEffect(()=>{
console.log(state)
},[state]) //count更新时执行
了解完了这些,我们接着完善我们的代码
export default ({ initNumber = 0 }: ItemProps) => {
const [state, setState] = useState(initNumber);
useEffect(() => {
layoutEmitter.emit({ state });
}, [state])
return <button
style={{ fontSize: '30px' }}
onClick={() => {
setState(state => state + 1)
setFlag(false)
}}
>EventEmitter {state} </button>
};
但是这样我们还是不解决问题,因为不管DependencyList
我们填什么,当index挂载到Dom上都会执行,然后传递过来一个initNumber
,那是不是就没有办法操作了呢?
我们可以再定义一个useState
,const [flag, setFlag] = useState(true);
来控制当我们没有点击按钮时不触发,useEffect
,然后将条件判断放在useEffect
内这样我们就可以达到目的
ps:不能将useEffect放在条件判断语句内,这是Hook的规范
// 1. initNumber需要定义参数类型
export default ({ initNumber = 0 }: ItemProps) => {
const [state, setState] = useState(initNumber);
const [flag, setFlag] = useState(true);
useEffect(() => {
if (!flag) {
layoutEmitter.emit({ state });
}
}, [state])
return <button
style={{ fontSize: '30px' }}
onClick={() => {
setState(state => state + 1)
setFlag(false)
}}
>EventEmitter {state} </button>
};
这样我们就实现了需求,并且按钮发生点时,在页面也会渲染出正确的数据。
以上实现由于本人目前技术不足,另辟蹊径的方法,这样实现可能会带来一些问题。
6.关于React setState的探讨
在掘金上一篇关于React 中 setState 是一个宏任务还是微任务? 文章下面有这么一条评论:
说得通俗一点,setState是一个伪异步,或者可以称为defer,即延迟执行但本身还在一个事件循环,所以它的执行顺序在同步代码后、异步代码前。为什么会有这种现象?这就要说到react的合成事件了,react的批处理更新也得益于合成事件,可以试下脱离react事件,使用原生事件执行setState,你会得到同步的代码。