一些组件也许想要更新状态来响应同一事件。下面这个例子是假设的,但是却说明了一个常见的模式:
function Parent() {
let [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
Parent clicked {count} times
<Child />
</div>
);
}
function Child() {
let [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Child clicked {count} times
</button>
);
}
当事件被触发时,子组件的 onClick 首先被触发(同时触发了它的 setState )。然后父组件在它自己的 onClick 中调用 setState 。
如果 React 立即重渲染组件以响应 setState 调用,最终我们会重渲染子组件两次:
*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
- setState
- re-render Child // 😞 不必要的重渲染
Parent (onClick)
- setState
- re-render Parent
- re-render Child
*** 结束 React 浏览器 click 事件处理过程 ***
第一次 Child 组件渲染是浪费的。并且我们也不会让 React 跳过 Child 的第二次渲染因为 Parent 可能会传递不同的数据由于其自身的状态更新。
这就是为什么 React 会在组件内所有事件触发完成后再进行批量更新的原因:
*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
- setState
Parent (onClick)
- setState
*** Processing state updates ***
- re-render Parent
- re-render Child
*** 结束 React 浏览器 click 事件处理过程 ***
组件内调用 setState 并不会立即执行重渲染。相反,React 会先触发所有的事件处理器,然后再触发一次重渲染以进行所谓的批量更新。
批量更新虽然有用但可能会让你感到惊讶如果你的代码这样写:
const [count, setCounter] = useState(0);
function increment() {
setCounter(count + 1);
}
function handleClick() {
increment();
increment();
increment();
}
如果我们将 count 初始值设为 0 ,上面的代码只会代表三次 setCount(1) 调用。为了解决这个问题,我们给 setState 提供了一个 “updater” 函数作为参数:
const [count, setCounter] = useState(0);
function increment() {
setCounter(c => c + 1);
}
function handleClick() {
increment();
increment();
increment();
}
React 会将 updater 函数放入队列中,并在之后按顺序执行它们,最终 count 会被设置成 3 并作为一次重渲染的结果。
当状态逻辑变得更加复杂而不仅仅只是少数的 setState 调用时,我建议你使用 useReducer Hook 来描述你的局部状态。它就像 “updater” 的升级模式在这里你可以给每一次更新命名:
const [counter, dispatch] = useReducer((state, action) => {
if (action === 'increment') {
return state + 1;
} else {
return state;
}
}, 0);
function handleClick() {
dispatch('increment');
dispatch('increment');
dispatch('increment');
}
action 字段可以是任意值,尽管对象是常用的选择。