
上次讲到了React的并发渲染,说到React18中基于Fiber的并发渲染机制,React可能会中断当前渲染,然后处理优先级更高的UI交互。那么在这个过程中,是否有些问题呢?我们今天接着说。
什么是Tearing
tearing字面意思是撕裂、撕开,对应着React就是页面的信息"被撕裂了"。意思就是UI渲染出来的结果不一致了。即同一个state在不同的组件里显示出了不同的状态,有一个或多个组件渲染了旧状态,有一个或多个组件渲染的最新的状态。
Tearing 通常发生在多个状态更新同时进行时,尤其是在使用并发特性(如useTransition
、Suspense
)时。当应用程序的状态发生变化,而 React 正在渲染一个组件的旧版本时,如果此时状态再次更新,那么在不同的渲染帧中,用户可能会看到部分界面显示的是旧状态,而另一部分显示的是新状态,从而导致视觉上的不一致。
为什么会发生Tearing
在传统的同步渲染中,React 会一次性完成整个组件的渲染,然后提交到 DOM,因此不会出现中间状态。但在并发模式下,React 可以暂停渲染过程去处理更高优先级的任务(比如用户输入),然后再回来继续渲染。这就可能导致:
- 多个状态更新交错进行:一个更新正在渲染中,另一个更新介入并改变了状态,导致先前的渲染结果可能已经过时。
- 部分更新提交:React 可能会分批次提交渲染结果(例如,由于Suspense边界),导致界面的不同部分显示不同状态的数据。
注意 :在React的中,tearing仅会在依赖了外部了外部状态的情况下才会发生,并且主要是发生在并发渲染中
为什么在并发渲染中容易出现
React 18 引入的并发渲染允许渲染过程被中断和恢复:
- 渲染开始:React 开始基于当前状态 State v1 生成 UI。
- 状态更新:此时外部状态突然变为 State v2。
- 渲染恢复:部分组件使用 State v1,部分使用 State v2,导致 UI 不一致。
代码演示:
javascript
// 外部状态(非 React 管理)
let externalState = 0;
const ComponentA = () => {
const [state, setState] = React.useState(externalState);
// 监听外部状态变化(非 React 标准方式)
React.useEffect(() => {
const interval = setInterval(() => {
externalState = Math.random(); // 外部状态突变
setState(externalState); // 触发 React 更新
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>ComponentA: {state}</div>;
};
const ComponentB = () => {
// 直接读取外部状态(可能过时)
return <div>ComponentB: {externalState}</div>;
};
function App() {
return (
<>
<ComponentA />
<ComponentB />
</>
);
}
问题
:ComponentA 通过 React 状态更新,而 ComponentB 直接读取外部状态。当 externalState 突变时,两者可能显示不同的值。
如何解决Tearing
React18提供了专用API,即useSyncExternalStore
来确保了状态的一致性:
- 直接使用useSyncExternalStore:
转为外部数据集成设计,保证状态读取的原子性。
javascript
import { useSyncExternalStore } from 'react';
// 定义外部 store
const store = {
state: 0,
listeners: new Set(),
setState(newState) {
this.state = newState;
this.listeners.forEach(listener => listener());
},
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot() {
return this.state;
}
};
const Component = () => {
// 使用 useSyncExternalStore 绑定外部 store
const state = useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
return <div>{state}</div>;
};
- 使用基于React的状态管理
主流库(Redux、Zustand)已经适配了useSyncExternalStore
:
javascript
// store.js
import { create } from 'zustand'
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
// BearCounter.jsx
function BearCounter() {
const bears = useStore((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
关于Tearing的思考
刚刚有说过,如果你使用的是外部数据管理才会有Tearing,那么如果使用React中的状态管理,即useState/useContext等就不会产生Tearing了。那么这是如何保证的呢?
其实这个还是得益于React的核心更新机制:
- 原子性快照:当 React 开始一次渲染时,它会为整个组件树捕获当前所有状态的一致性快照。即使有交错的状态更新:
scss
// 示例:交错的状态更新
const [count, setCount] = useState(0);
const [text, setText] = useState('');
useEffect(() => {
setCount(1); // 更新1
setText('Hello'); // 更新2
}, []);
React 会将这两个更新批处理,并在下一次渲染中使用统一的新状态值。
- 不可变状态读取:在渲染过程中,组件读取的状态值来自 "渲染开始时确定的快照"。即使更新在渲染中途被触发:
javascript
const ComponentA = () => {
const [value] = useState(0);
// 即使其他组件在此处触发更新...
return <div>{value}</div>; // 仍显示渲染开始时的值
};
纯React中的状态不会引起tearing
javascript
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
// 模拟快速交错更新
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // 更新1
setCount(c => c + 1); // 更新2(交错)
}, 100);
return () => clearInterval(interval);
}, []);
return (
<>
<ComponentA count={count} />
<ComponentB count={count} />
</>
);
}
// 两个组件接收相同的props
const ComponentA = ({ count }) => <div>A: {count}</div>;
const ComponentB = ({ count }) => <div>B: {count}</div>;
在此示例中:
- 即使 setCount 被快速连续调用
- 即使使用并发渲染(如 createRoot)
- ComponentA 和 ComponentB 总是显示相同的值
Tearing只发送在下图场景:

为什么外部状态会导致tearing
- 脱离 React 调度:外部状态更新不受 React 批处理和优先级控制
- 读取非原子性:不同组件在不同时间点读取值可能不同
javascript
// 伪代码演示危险操作
let externalState = 0;
const updateState = () => {
externalState = Date.now(); // 突变外部状态
};
// 组件A
const A = () => <div>{externalState}</div>;
// 组件B
const B = () => {
// 如果渲染在此处暂停,A和B可能显示不同值
return <div>{externalState}</div>;
};
结论:
场景 | 是否可能发生 tearing | 原因 |
---|---|---|
纯 React 状态(useState/useContext) | ❌ 不可能 | React 保证原子性渲染 |
外部状态 + useSyncExternalStore | ❌ 不可能 | React 18+ 提供原子读取 |
直接操作外部状态 | ✅ 可能 | 脱离 React 控制机制 |
关键总结: 只要使用 React 内置状态管理且不直接操作外部可变状态,即使在并发渲染下,React 也绝对保证不会发生 tearing。这是 React 并发模型设计的核心安全特性之一。
喜欢就点个关注,不定期分享一些技术细节、感悟、新鲜事等。感谢!