类组件可以使用 setState
更新状态。
函数组件可以使用 const [value, setValue] = useState(0)
中的 setXXX
来更新状态。
并且这些设置状态的函数可能在一个函数或者类中出现很多次,那么 React 什么时候来决定重新渲染呢?
- 如果多个状态更新在同一个
React-based
事件中,例如按钮点击事件,输入框变更事件等,那么这些事件中的多个setState
和setXXX
会批量缓存,直到该事件结束,才开始下一次渲染。例如下边代码中的update()
点击中的所有状态变更会 batch,从而只有一次渲染。
function Batched() {
const countRef = useRef(1);
const [a, setA] = useState(0);
const [b, setB] = useState(1);
const [f, setF] = useState(2);
function update() {
// 由于在 React 同步事件处理范围内,所有同步的状态更新会 batch
setA(v => v + 1);
setB(v => v + 1);
setF(v => v + 1);
}
useEffect(() => {
countRef.current += 1;
});
return (
<>
<h1>渲染 {countRef.current} 次</h1>
<p>a = {a}</p>
<p>b = {b}</p>
<p>f = {f}</p>
<button onClick={update}>点击按钮触发一次渲染</button>
</>
);
}
- 如果状态更新不在
React-based
事件中,例如setTimeout
,setInterval
,async 函数
,promises
等等,在这些函数中的状态更新不会批量缓存,一个状态变更触发一次组件新的渲染。
function NotBatched() {
const countRef = useRef(1);
const [a, setA] = useState(0);
const [b, setB] = useState(1);
const [f, setF] = useState(2);
function update() {
// 点击事件后,第二次渲染
setA(v => v + 1);
setTimeout(() => {
// 由于不在 React 同步事件处理范围,这个函数里边所有状态更新不会缓存
// setInterval,async 函数同理,参见下边的 asyncUpdate 函数
// 这个会触发第三次渲染
setB(v => v + 1);
// 这个会触发第四次渲染
setF(v => v + 1);
}, 1000);
}
async function asyncUpdate() {
// 点击后第 1 次渲染
setA(v => v + 1);
await waiting(1000);
// 点击后第 2 次渲染
setB(v => v + 1);
// 点击后第 3 次渲染
setF(v => v + 1);
}
useEffect(() => {
countRef.current += 1;
});
return (
<>
<h1>渲染 {countRef.current} 次</h1>
<p>a = {a}</p>
<p>b = {b}</p>
<p>f = {f}</p>
<button onClick={update}>点击按钮触发三次渲染</button>
</>
);
}
上面所说的 React-based
事件中的状态更新会缓存,其实底层依赖的是 unstable_batchedUpdates
函数,这个函数会将事件处理函数包裹起来。
类似的,我们在定时器函数、Promises,async 函数中也可以手动调用该方法,达到批量缓存的目的。
import { unstable_batchedUpdates } from 'react-dom';
function ManualBatched() {
const countRef = useRef(1);
const [a, setA] = useState(0);
const [b, setB] = useState(1);
const [f, setF] = useState(2);
function update() {
// 点击事件后,第二次渲染
setA(v => v + 1);
setTimeout(() => {
unstable_batchedUpdates(() => {
// 这两个状态变更会 batch
setB(v => v + 1);
setF(v => v + 1);
});
}, 1000);
}
async function asyncUpdate() {
// 点击后第 1 次渲染
setA(v => v + 1);
await waiting(1000);
unstable_batchedUpdates(() => {
// 这两个状态变更会 batch
setB(v => v + 1);
setF(v => v + 1);
});
}
useEffect(() => {
countRef.current += 1;
});
return (
<>
<h1>渲染 {countRef.current} 次</h1>
<p>a = {a}</p>
<p>b = {b}</p>
<p>f = {f}</p>
<button onClick={update}>点击按钮触发三次渲染</button>
</>
);
}
将一些逻辑相关的变量组合成一个对象,因为 useState 中可以是任何值,更新时使用扩展运算符。
setState(prevState => {
return {...prevState, loading, data};
});
一个例子如下:
import {useReducer} from 'react'
import {isEqual} from 'lodash';
const getData = url => {
// setState 一般为派发函数 dispatch,这里将新的对象合并到已有对象(或者覆盖已有值)
const [state, setState] = useReducer(
(oldState, newState) => {
const mergeState = {...oldState, ...newState};
// 如果值没有变更,那么返回原始引用,避免依赖 state 的接口请求
if (isEqual(oldState, mergeState)) {
return oldState;
}
return mergeState;
},
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().
React wraps your event handlers in a call to unstable_batchedUpdates(), so that your handler runs inside a callback. Any state updates triggered inside that callback will be batched. Any state updates triggered outside that callback will not be batched. Timeouts, promises, and async functions will end up executing outside that callback, and therefore not be batched.