tearing ,来自图形编程的一个术语,是指视觉上的不一致,比如闪动阴影
伴随着 React 18 的并行渲染,出现了 tearing,并行渲染的出现,大幅度提升了用户体验,它意味着 React 在渲染过程中,还可以被更高优先级的任务打断的,比如用户操作,而在此区间,就会出现暂时的渲染抖动,在看 tearing 出现的场景前,我们对比下 React 18 的渲染与之前有何不同
以下图为例
在第一张图中,我们可以看到,组件访问一个外部 store 获取 state,并将他渲染至组件中
在第二张图中,其他组件也来渲染这个 state,由于我们的渲染过程过程不会被打断,所以渲染至第二个组件中
在第三张图中,我们可以看到,所有的组件均渲染完成,这个时候我们可以看到页面会呈现统一的 UI
那么如果 external store 发生变化,我们涉及到的组件,会从第一张图从头来过

仍以下图为例
在第一张图中,初始 external store 是蓝色,第一个组件渲染成蓝色,没有问题
在第二张图中,假设用户点击了按钮,将 external store 改成红色,这个时候,渲染会被打断,react 会让用户操作生效,来提升用户体验
在第三张图中,其他组件继续渲染,拿到改变后的 external store,渲染成红色
在第四张图中,几个组件渲染完成,但是由于并行渲染的问题,并不会检测到 external store 的变化,去重新将所有组件再渲染一遍,所以就导致了同一个数据,被展示不同的值,这种情况,就是 tearing

import { useEffect, useState, startTransition } from 'react';
let externalState = { counter: 0 };
let listeners: any[] = [];
function dispatch(action: { type: any }) {
if (action.type === 'increment') {
externalState = { counter: externalState.counter + 1 };
} else {
throw Error('Unknown action');
}
listeners.forEach((fn) => fn());
}
function subscribe(fn: () => void) {
listeners = [...listeners, fn];
return () => {
listeners = listeners.filter((f) => f !== fn);
};
}
function useExternalData() {
const [state, setState] = useState(externalState);
useEffect(() => {
const handleChange = () => setState(externalState);
const unsubscribe = subscribe(handleChange);
return unsubscribe;
}, []);
return state;
}
setInterval(() => {
dispatch({ type: 'increment' });
}, 50);
export default function App() {
const [show, setShow] = useState(false);
return (
<div className="App">
<button
onClick={() => {
startTransition(() => {
setShow(!show);
});
}}
>
toggle content
</button>
{show && (
<>
<SlowComponent />
<SlowComponent />
<SlowComponent />
<SlowComponent />
<SlowComponent />
</>
)}
</div>
);
}
function SlowComponent() {
let now = performance.now();
while (performance.now() - now < 200) {
// do nothing
}
const state = useExternalData();
return <h3>Counter: {state.counter}</h3>;
}
上面的代码,我们模拟了一个外部状态,当我们点击按钮,会看到,同一个 state 会出现不同的值,这种情况就是 tearing
那么既然 tearing 是由 external store 引起,那么接下来我们看常用的一些状态管理
既然对于 external store,会存在 tearing,那么我们看一些比较常见的状态管理工具是如何应对的
一个轻量简单的状态管理工具,可以将业务逻辑从上下文中提取出来,便于管理 state,集成 immer,保证最少的 rerender,善用 immer,减少 rerender
// store.ts
import create from 'zustand/vanilla';
import { immer } from 'zustand/middleware/immer';
const DEFAULT_STATE = {
name: 'duanxl.com',
age: 20,
arr: ['201603'],
};
// const store = create(() => ({ ...DEFAULT_STATE }));
const store = create(
immer<typeof DEFAULT_STATE>((set) => ({
...DEFAULT_STATE,
}))
);
const { getState, setState, subscribe, destroy } = store;
subscribe((state) => {
console.log('state更新了', state.name);
});
function changeState() {
// 函数体会执行,值相同的话,按理不会造成rerender,但是不尽然
// setState({
// ...DEFAULT_STATE,
// name: 'duanxl.com' + Math.random(),
// });
// bad case 函数体会执行,每次造成rerender
// setState({...res})
// setState({
// name: 'duanxl.com',
// age: 20,
// });
// store.setState((state) => {
// state.name = Math.random().toString();
// state.age = 10;
// });
store.setState((state) => {
state.arr = ['201603'];
});
}
export { store, changeState, getState };
针对上面的 bad case,我们可以自定义 immer,支持自定义 setSate,达到精细变更,更贴近业务逻辑
import produce from 'immer';
export const useStore = create<{
lush: { forest: { contains: { a: string } } };
clearForest: (data: string) => void;
}>((set) => ({
lush: { forest: { contains: { a: 'bear' } } },
clearForest: (data: string) =>
set(
produce((state) => {
// console.log('state', data);
// state = data;
// return data;
state.lush.forest.contains.a = data;
})
),
}));
根据这样的自定义方式,如果改变的不是指定路径,会直接阻止提交,并抛出异常,可以以此来约束开发规范
store.setState((state) => {
state.lush = state; // Error
});
store.setState((state) => {
state.lush = { forest: { contains: { a: 'bear' } } }; // success 但是会rerender
});
针对上面的路径一样问题,我们可以在 setState 中抛出异常,以阻止不是原子 state 带来的变更
用法简单,api 较少,但是目前针对 react18 版本,immer 失效,可能之后会解决吧,上面的 zustand 也存在这样的问题,和 zustand 一样,仍是一个作者(团队)开发,其中还有比较出名的 react-three-fiber 等经典库
import { useImmer } from '@hooks/useImmer';
import { atom, useAtom } from 'jotai';
import { atomWithImmer, useImmerAtom } from 'jotai/immer';
import { memo, useCallback, useEffect, useState } from 'react';
const mangaAtom = atom({ str: '状态测试' });
const mangaAtomObj = atomWithImmer({ str: '状态测试' });
function Courses() {
// const [data, setData] = useState({ str: '状态测试' });
const [data, setData] = useImmer({ str: '状态测试' });
console.log('rerender...');
// const [data, setData] = useAtom(mangaAtomObj);
// const [data, setData] = useAtom(mangaAtom);
// function becomeRicher() {
// setData((draft) => {
// draft.str = '状态测试';
// });
// }
const fn = useCallback(() => {
setData((draft) => {
draft.str = '状态测试';
// return draft;
});
// setData({ str: '状态测试' });
}, []);
// useEffect(() => {
// console.log('rerender...', data.str);
// }, [data]);
return (
<div>
<h2 onClick={fn}>{data.str}</h2>
<hr />
</div>
);
}
export default memo(Courses);
与其他状态管理工具相比,xstate 有状态机,适用于复杂业务,可追溯状态,统一 Api,不同技术栈可以共用一套状态管理,比如可以在微前端中解决主站与子站的通信等问题
import { createMachine, interpret } from 'xstate';
// Stateless machine definition
// machine.transition(...) is a pure function used by the interpreter.
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
// Machine instance with internal state
const toggleService = interpret(toggleMachine)
.onTransition((state) => console.log(state.value))
.start();
// => 'inactive'
toggleService.send('TOGGLE');
// => 'active'
toggleService.send('TOGGLE');
// => 'inactive'
import { DemoData, toggleMachine } from '@duanxl/utils';
import { atomWithMachine } from 'jotai/xstate';
interface EventObject {
type: string;
}
const toggleMachineAtom = atomWithMachine<
unknown,
EventObject,
{
value: 'inactive' | 'active';
context: unknown;
}
>(() => toggleMachine);
export { toggleMachineAtom };
Facebook 提供,atom state 管理工具,支持派生数据与异步查询,让业务更小更灵活
这里我们使用官方实例演示
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
console.log('rerender......')
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
为什么 external store 会出现 rerender 的问题,就是为了解决 tearing
当然,在 react wg 中,也讨论过这个问题,官方给出的答复是利用 useSyncExternalStore 解决这个问题,敬请期待…
上面的状态管理工具,很多都集成了 immer,看一眼 immer 解决了什么,为什么要用 immer
比如我们有一个 state,是一个层级超过 2 层的对象,那么我们每次 setState 的时候,都要使用…(rest 表达式)来编写,而且容易出错,写起来也比较复杂,重要的还是我们得很了解这个对象的属性
那么有人就说了,直接 setState 时候只改变对象的属性就好了,但是有一个问题,就是对于没有改变的属性,进行了无意义的拷贝,而且和原来的对象引用地址也变了,就会触发很多不必要的 render
immer 为了解决问题,提供了变更对象属性的方法, 就是改变对象后,引用地址不变,避免组件不必要的 render
import { cloneDeep } from 'lodash';
const initialState={
data:{
site:'duanxl.com'
}
}
const state = cloneDeep(initialState);
console.log(state.data === initialState.data); // false
import produce from 'immer';
const state = produce(initialState, () => {});
console.log(state.data === initialState.data); // true
那么 immer 是如何实现这个功能的呢
在没有了解 immer 之前,我想的是,就是深比较,使用 rest 表达式不断的赋值,但是 immer 不是这么做的,immer 使用 proxy 实现
const state={
data:{
site:'duanxl.com'
}
}
let changeState={};
const handler={
set(target, key, value) {
copy = {...target};
copy[key] = value;
}
}
const proxy=new Proxy(state,handler)
proxy.data.site='https://duanxl.com'
console.log(changeState.data === state.data) // true
function immer (state, thunk) {
let momory = new Map() // 缓存修改过的对象
const handler = {
get (target, key) {
return new Proxy(target[key], handler) // get拦截,返回proxy target[key] 确保是同一个handler
},
set (target, key, value) {
const copy = { ...target } // 浅拷贝
copy[key] = value // 赋值
momory.set(target, copy)
}
}
const finalize = (state) => {
const result = { ...state }
Object.keys(state).map((key) => {
if (momory.has(state[key])) {
result[key] = momory.get(state[key])
} else {
result[key] = state[key]
}
})
return result
}
const proxy = new Proxy(state, handler)
thunk(proxy)
return finalize(state)
}
原理中我们可以看到,如果使用 immer,尽量不要直接使用 useEffect 依赖源对象,会造成不必要的 hook 执行,也可能影响之后的逻辑
善用 React 三大件