面试题答案
一键面试React 18 之前和之后的 State 更新批处理机制不同点
- React 18 之前:
- 批处理范围有限:仅在 React 合成事件(如 onClick、onChange 等)和生命周期函数内会进行批处理。也就是说,在这些函数内部多次调用
setState
,React 会将这些状态更新合并成一次,从而避免不必要的重新渲染。例如:
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); // React 会将这两次 setState 合并,最终只重新渲染一次 }; render() { return <button onClick={this.handleClick}>{this.state.count}</button>; } }
- 非合成事件中不批处理:在原生 DOM 事件(如通过
addEventListener
直接绑定到 DOM 元素的事件)、setTimeout
、setInterval
或 Promise 的回调函数中调用setState
,React 不会进行批处理,每次调用setState
都会立即触发重新渲染。例如:
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { document.addEventListener('click', () => { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); // 这两次 setState 会触发两次重新渲染 }); } render() { return <div>{this.state.count}</div>; } }
- 批处理范围有限:仅在 React 合成事件(如 onClick、onChange 等)和生命周期函数内会进行批处理。也就是说,在这些函数内部多次调用
- React 18 之后:
- 自动批处理范围扩大:不仅在 React 合成事件和生命周期函数内,在
setTimeout
、setInterval
、原生 DOM 事件、Promise 的回调函数等任何内部,只要是 React 应用内部的更新,都会进行批处理。例如:
import React, { useState } from'react'; const Example = () => { const [count, setCount] = useState(0); const handleClick = () => { setTimeout(() => { setCount(count + 1); setCount(count + 1); // React 18 中这两次 setCount 会合并成一次更新 }, 0); }; return <button onClick={handleClick}>{count}</button>; };
- 并发模式支持:React 18 引入了并发模式,批处理机制为并发模式提供了更好的支持。它允许 React 在不阻塞主线程的情况下,对状态更新进行优化和调度,提高应用的响应性和性能。
- 自动批处理范围扩大:不仅在 React 合成事件和生命周期函数内,在
为什么要进行这样的改变
- 提高一致性:React 18 之前批处理机制在不同类型事件中的不一致性给开发者带来了困扰。扩大批处理范围可以让开发者无需关心事件类型,编写代码时更具一致性,减少意外的重新渲染,提高性能优化的可预测性。
- 适应新的应用场景:随着 React 应用越来越复杂,并发模式的引入需要更好的批处理机制来管理状态更新。批处理机制的改变使得 React 能够更好地调度任务,在不阻塞主线程的情况下处理多个状态更新,提升应用的用户体验。
在实际项目中受影响的场景及解决方法
- 场景:
- 依赖中间状态的场景:在 React 18 之前,非合成事件等情况下多次
setState
立即执行,开发者可能依赖这种立即执行特性获取中间状态。例如,在原生 DOM 事件中多次setState
并在后续代码中依赖更新后的状态:
在 React 18 批处理机制下,上述代码获取的class Example extends React.Component { constructor(props) { super(props); this.state = { data: [] }; } componentDidMount() { document.addEventListener('click', () => { this.setState({ data: [...this.state.data, 'item1'] }); // 这里依赖立即更新后的 state.data 去做一些操作 const newData = this.state.data.filter(item => item!== 'item1'); this.setState({ data: newData }); }); } render() { return <div>{this.state.data.join(', ')}</div>; } }
this.state.data
并不是更新后的状态,因为批处理延迟了状态更新。 - 依赖中间状态的场景:在 React 18 之前,非合成事件等情况下多次
- 解决方法:
- 使用回调形式的 setState:在 React 18 之前和之后都可以使用回调形式的
setState
来确保获取到最新状态。例如:
class Example extends React.Component { constructor(props) { super(props); this.state = { data: [] }; } componentDidMount() { document.addEventListener('click', () => { this.setState((prevState) => ({ data: [...prevState.data, 'item1'] }), () => { const newData = this.state.data.filter(item => item!== 'item1'); this.setState({ data: newData }); }); }); } render() { return <div>{this.state.data.join(', ')}</div>; } }
- 使用 useReducer:
useReducer
可以更好地管理复杂状态更新逻辑,它返回的dispatch
函数在 React 18 批处理机制下也能按预期工作。例如:
这样可以避免依赖中间状态带来的问题,同时利用 React 18 的批处理机制优化性能。import React, { useReducer } from'react'; const initialState = { data: [] }; const reducer = (state, action) => { switch (action.type) { case 'addItem': return { data: [...state.data, 'item1'] }; case'removeItem': return { data: state.data.filter(item => item!== 'item1') }; default: return state; } }; const Example = () => { const [state, dispatch] = useReducer(reducer, initialState); const handleClick = () => { dispatch({ type: 'addItem' }); dispatch({ type:'removeItem' }); }; return ( <div> <button onClick={handleClick}>操作</button> {state.data.join(', ')} </div> ); };
- 使用回调形式的 setState:在 React 18 之前和之后都可以使用回调形式的