react18剖析

react 剖析

使用脚手架搭建 react 项目

vite

1
npm create vite@latest my-react-app --template react-ts

create-react-app

1
npx create-react-app my-react-app --template typescript

JSX

什么是 JSX

JSX(JavaScript XML)是一种 JavaScript 的语法扩展,允许我们在 JavaScript 代码中编写类似 HTML 的结构。它使得组件的结构更加直观和易于维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JSX 示例
const element = <h1 className="greeting">Hello, React!</h1>;

// 带表达式的 JSX
const name = "World";
const element = <h1>Hello, {name}!</h1>;

// 嵌套结构
const component = (
<div className="container">
<h1>标题</h1>
<p>这是一段文本</p>
</div>
);

JSX 编译原理

JSX 通过编译器(Babel 的 @babel/preset-react 或 plugin-transform-react-jsx)进行转换。

React 17 之前的转换方式:

1
2
3
4
5
6
7
8
9
// 源代码
const element = <h1 className="greeting">Hello, React!</h1>;

// 编译后(旧)
const element = React.createElement(
"h1",
{ className: "greeting" },
"Hello, React!"
);

React 17+ 的新 JSX 转换:

1
2
3
4
5
6
7
8
9
// 源代码
const element = <h1 className="greeting">Hello, React!</h1>;

// 编译后(新)
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("h1", {
className: "greeting",
children: "Hello, React!",
});

在线转换地址:Babel 在线转换工具

JSX 规则

  1. 必须返回单个根元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 错误
    return (
    <h1>标题</h1>
    <p>段落</p>
    );

    // 正确 - 使用父元素包裹
    return (
    <div>
    <h1>标题</h1>
    <p>段落</p>
    </div>
    );

    // 正确 - 使用 Fragment
    return (
    <>
    <h1>标题</h1>
    <p>段落</p>
    </>
    );
  2. 所有标签必须闭合

    1
    2
    3
    <img src="image.jpg" />
    <br />
    <input type="text" />
  3. 使用驼峰命名法

    1
    2
    3
    <div className="container" onClick={handleClick}>
    <label htmlFor="input">标签</label>
    </div>

Hooks

Hooks 是 React 16.8 引入的特性,让函数组件能够使用状态和其他 React 特性。

useState

用于在函数组件中添加状态管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount((prev) => prev - 1)}>减少</button>
</div>
);
}

重点:

  • 状态更新是异步的
  • 使用函数式更新可以获取最新状态:setCount(prev => prev + 1)
  • 初始值只在首次渲染时使用

useReducer

适合管理复杂的状态逻辑,类似于 Redux 的 reducer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
throw new Error("Unknown action type");
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>重置</button>
</div>
);
}

useRef

创建一个可变的引用对象,用于访问 DOM 元素或存储不触发重渲染的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRef, useEffect } from "react";

function TextInput() {
const inputRef = useRef(null);
const renderCount = useRef(0);

useEffect(() => {
renderCount.current += 1;
inputRef.current.focus();
});

return (
<div>
<input ref={inputRef} type="text" />
<p>组件渲染次数: {renderCount.current}</p>
</div>
);
}

useRef 的两个主要用途:

  1. 访问 DOM 元素
  2. 存储不触发重渲染的可变值

useMemo

用于缓存计算结果,避免在每次渲染时进行昂贵的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useMemo, useState } from "react";

function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState("");

// 只有当 items 或 filter 改变时才重新计算
const filteredItems = useMemo(() => {
console.log("过滤计算执行");
return items.filter((item) =>
item.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);

return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="搜索..."
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}

useCallback

返回一个记忆化的回调函数,避免子组件不必要的重渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useCallback, useState, memo } from "react";

// 子组件使用 memo 包裹
const Button = memo(({ onClick, children }) => {
console.log("Button 渲染");
return <button onClick={onClick}>{children}</button>;
});

function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);

// 只有 count 改变时才创建新函数
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);

return (
<div>
<p>Count: {count}</p>
<p>Other: {other}</p>
<Button onClick={increment}>增加 Count</Button>
<button onClick={() => setOther((o) => o + 1)}>增加 Other</button>
</div>
);
}

useMemo vs useCallback:

  • useMemo(() => fn) 缓存函数的返回值
  • useCallback(fn) 缓存函数本身

useContext

用于在组件树中共享数据,避免 props 层层传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { createContext, useContext, useState } from "react";

// 创建 Context
const ThemeContext = createContext();

function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");

const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

// 使用 Context
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);

return (
<button
onClick={toggleTheme}
style={{
background: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#333" : "#fff",
}}
>
当前主题: {theme}
</button>
);
}

function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}

useEffect

处理副作用(数据获取、订阅、DOM 操作等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useEffect, useState } from "react";

function DataFetcher({ userId }) {
const [data, setData] = useState(null);

useEffect(() => {
let cancelled = false;

async function fetchData() {
const response = await fetch(`/api/user/${userId}`);
const json = await response.json();

if (!cancelled) {
setData(json);
}
}

fetchData();

// 清理函数
return () => {
cancelled = true;
};
}, [userId]); // 依赖项数组

return <div>{data ? data.name : "加载中..."}</div>;
}

useLayoutEffect

与 useEffect 类似,但在所有 DOM 变更后同步触发,用于需要同步测量布局的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useLayoutEffect, useRef, useState } from "react";

function MeasureComponent() {
const ref = useRef(null);
const [height, setHeight] = useState(0);

useLayoutEffect(() => {
// 在浏览器绘制前执行
setHeight(ref.current.offsetHeight);
}, []);

return (
<div ref={ref}>
<p>组件高度: {height}px</p>
</div>
);
}

ref

ref 不仅可以作为 DOM 的引用,还可以作为不需要引起视图更新的数据存储。

ref 的使用场景

  1. 访问 DOM 元素
  2. 存储不触发重渲染的值(如定时器 ID、前一个 props 值等)
  3. 与第三方库集成(如视频播放器、图表库等)
1
2
3
4
5
6
7
8
9
10
11
12
import { useRef, useEffect } from "react";

function App() {
const ref = useRef(null);

useEffect(() => {
console.log(ref.current); // 输出 DOM 元素
ref.current.focus(); // 聚焦输入框
}, []);

return <div ref={ref}>Ref demo</div>;
}

ref 的常见用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function VideoPlayer({ src }) {
const videoRef = useRef(null);

const play = () => {
videoRef.current.play();
};

const pause = () => {
videoRef.current.pause();
};

return (
<div>
<video ref={videoRef} src={src} />
<button onClick={play}>播放</button>
<button onClick={pause}>暂停</button>
</div>
);
}

forwardRef

forwardRef 允许组件将 ref 转发给子组件,使父组件能够访问子组件的 DOM 节点。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { forwardRef, useRef } from "react";

// 子组件使用 forwardRef 包裹
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});

// 父组件
function Form() {
const inputRef = useRef(null);

const handleFocus = () => {
inputRef.current.focus();
};

return (
<div>
<CustomInput ref={inputRef} placeholder="输入文本" />
<button onClick={handleFocus}>聚焦输入框</button>
</div>
);
}

结合 useImperativeHandle

使用 useImperativeHandle 可以自定义暴露给父组件的实例值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { forwardRef, useRef, useImperativeHandle } from "react";

const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);

useImperativeHandle(ref, () => ({
// 只暴露特定的方法
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = "";
},
getValue: () => {
return inputRef.current.value;
},
}));

return <input ref={inputRef} {...props} />;
});

function App() {
const fancyInputRef = useRef(null);

const handleClick = () => {
fancyInputRef.current.focus();
console.log(fancyInputRef.current.getValue());
fancyInputRef.current.clear();
};

return (
<div>
<FancyInput ref={fancyInputRef} placeholder="自定义输入框" />
<button onClick={handleClick}>操作输入框</button>
</div>
);
}

Suspense

Suspense 允许你在组件树中指定加载状态,通常用于代码分割和数据获取。

与 lazy 配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Suspense, lazy } from "react";

// 懒加载组件
const LazyComponent = lazy(() => import("./LazyComponent"));

function App() {
return (
<div>
<h1>我的应用</h1>
<Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}

嵌套 Suspense

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Suspense, lazy } from "react";

const Profile = lazy(() => import("./Profile"));
const Posts = lazy(() => import("./Posts"));

function App() {
return (
<Suspense fallback={<div>加载页面...</div>}>
<div>
<Suspense fallback={<div>加载个人资料...</div>}>
<Profile />
</Suspense>
<Suspense fallback={<div>加载文章...</div>}>
<Posts />
</Suspense>
</div>
</Suspense>
);
}

React 18 中的数据获取

React 18 支持在 Suspense 中进行数据获取(需要配合支持 Suspense 的数据获取库)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Suspense } from "react";

function UserProfile({ userId }) {
// 使用支持 Suspense 的数据获取库(如 React Query、SWR 等)
const user = useUser(userId); // 这会在数据未准备好时 suspend

return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

function App() {
return (
<Suspense fallback={<div>加载用户信息...</div>}>
<UserProfile userId={1} />
</Suspense>
);
}

lazy

React.lazy 用于动态导入组件,实现代码分割,减少初始加载时间。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { lazy, Suspense } from "react";

// 动态导入组件
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
const [page, setPage] = useState("dashboard");

return (
<div>
<nav>
<button onClick={() => setPage("dashboard")}>仪表板</button>
<button onClick={() => setPage("settings")}>设置</button>
</nav>

<Suspense fallback={<div>加载中...</div>}>
{page === "dashboard" && <Dashboard />}
{page === "settings" && <Settings />}
</Suspense>
</div>
);
}

路由级别的代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Contact = lazy(() => import("./pages/Contact"));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>页面加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

错误处理

结合 Error Boundary 处理加载失败的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Component, lazy, Suspense } from "react";

class ErrorBoundary extends Component {
state = { hasError: false };

static getDerivedStateFromError(error) {
return { hasError: true };
}

componentDidCatch(error, info) {
console.error("组件加载失败:", error, info);
}

render() {
if (this.state.hasError) {
return <div>组件加载失败,请刷新页面重试</div>;
}
return this.props.children;
}
}

const LazyComponent = lazy(() => import("./LazyComponent"));

function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}

memo

React.memo 是一个高阶组件,用于优化函数组件的性能,避免不必要的重渲染。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { memo, useState } from "react";

// 使用 memo 包裹组件
const ExpensiveComponent = memo(({ value }) => {
console.log("ExpensiveComponent 渲染");
return <div>值: {value}</div>;
});

function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");

return (
<div>
<ExpensiveComponent value={count} />
<button onClick={() => setCount(count + 1)}>增加计数</button>

<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入不会触发 ExpensiveComponent 重渲染"
/>
</div>
);
}

自定义比较函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { memo } from "react";

const User = memo(
({ user }) => {
console.log("User 组件渲染");
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
},
(prevProps, nextProps) => {
// 返回 true 表示不需要重渲染
// 返回 false 表示需要重渲染
return prevProps.user.id === nextProps.user.id;
}
);

function App() {
const [user, setUser] = useState({
id: 1,
name: "张三",
email: "zhang@example.com",
});
const [count, setCount] = useState(0);

return (
<div>
<User user={user} />
<button onClick={() => setCount(count + 1)}>
计数: {count} (不会触发 User 重渲染)
</button>
<button onClick={() => setUser({ ...user, name: "李四" })}>
改变用户 (会触发 User 重渲染)
</button>
</div>
);
}

memo 的使用场景

  1. 组件渲染开销大:组件包含复杂的计算或大量的 DOM 节点
  2. props 不经常变化:组件的 props 在多次渲染中保持不变
  3. 纯展示组件:组件只依赖 props,没有内部状态

注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { memo, useState } from "react";

// 错误示例:每次渲染都会创建新对象,导致 memo 失效
function App() {
const [count, setCount] = useState(0);

return (
<MemoChild
data={{ value: count }} // 每次都是新对象
onClick={() => {}} // 每次都是新函数
/>
);
}

// 正确示例:配合 useMemo 和 useCallback
function App() {
const [count, setCount] = useState(0);

const data = useMemo(() => ({ value: count }), [count]);
const handleClick = useCallback(() => {}, []);

return <MemoChild data={data} onClick={handleClick} />;
}

React 18 新特性

React 18 引入了多项重大更新,提升了性能和用户体验。

并发渲染(Concurrent Rendering)

React 18 的核心特性,允许 React 同时准备多个版本的 UI,并在需要时中断渲染。

关键概念:

  • 可中断的渲染:React 可以暂停渲染工作,处理更高优先级的更新
  • 自动批处理:多个状态更新会自动批处理成一次重渲染
  • 过渡更新:区分紧急更新和非紧急更新

自动批处理(Automatic Batching)

React 18 会自动批处理所有状态更新,无论它们在哪里触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useState } from "react";

function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
// React 18: 自动批处理,只触发一次重渲染
setCount((c) => c + 1);
setFlag((f) => !f);
// 在 React 17 中,如果在 Promise、setTimeout 中,会触发两次渲染
}

setTimeout(() => {
// React 18: 这里也会批处理
setCount((c) => c + 1);
setFlag((f) => !f);
}, 1000);

console.log("渲染"); // 只输出一次

return (
<div>
<button onClick={handleClick}>
Count: {count}, Flag: {flag.toString()}
</button>
</div>
);
}

退出批处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { flushSync } from "react-dom";

function handleClick() {
flushSync(() => {
setCount((c) => c + 1);
});
// 此时 DOM 已更新

flushSync(() => {
setFlag((f) => !f);
});
// 又更新了一次
}

Transitions API

用于标记非紧急的状态更新,让 React 知道某些更新可以被中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useState, useTransition } from "react";

function SearchResults() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);

function handleChange(e) {
const value = e.target.value;

// 紧急更新:立即更新输入框
setQuery(value);

// 非紧急更新:搜索结果可以延迟
startTransition(() => {
const filtered = largeList.filter((item) => item.includes(value));
setResults(filtered);
});
}

return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? (
<div>搜索中...</div>
) : (
<ul>
{results.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}

useDeferredValue

延迟更新某个值,让紧急更新优先处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useState, useDeferredValue, memo } from "react";

function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);

return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* 输入框立即响应 */}
<p>当前输入: {text}</p>

{/* 列表渲染使用延迟的值 */}
<SlowList text={deferredText} />
</div>
);
}

const SlowList = memo(({ text }) => {
const items = [];
for (let i = 0; i < 250; i++) {
items.push(
<li key={i}>
{text} - {i}
</li>
);
}
return <ul>{items}</ul>;
});

useId

生成唯一的 ID,适用于服务端渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useId } from "react";

function NameFields() {
const id = useId();

return (
<div>
<label htmlFor={`${id}-firstName`}>名字:</label>
<input id={`${id}-firstName`} type="text" />

<label htmlFor={`${id}-lastName`}>姓氏:</label>
<input id={`${id}-lastName`} type="text" />
</div>
);
}

// 即使有多个组件实例,ID 也不会冲突
function App() {
return (
<>
<NameFields />
<NameFields />
</>
);
}

useSyncExternalStore

用于订阅外部数据源,确保在并发渲染中的一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useSyncExternalStore } from "react";

// 外部 store
const store = {
state: { count: 0 },
listeners: new Set(),

subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},

getSnapshot() {
return this.state;
},

increment() {
this.state = { count: this.state.count + 1 };
this.listeners.forEach((listener) => listener());
},
};

function Counter() {
const state = useSyncExternalStore(
store.subscribe.bind(store),
store.getSnapshot.bind(store)
);

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => store.increment()}>增加</button>
</div>
);
}

useInsertionEffect

专门用于 CSS-in-JS 库,在 DOM 变更前同步触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useInsertionEffect } from "react";

function useCSS(rule) {
useInsertionEffect(() => {
// 在浏览器重新计算布局前插入样式
const style = document.createElement("style");
style.textContent = rule;
document.head.appendChild(style);

return () => {
document.head.removeChild(style);
};
}, [rule]);
}

function App() {
useCSS(`
.dynamic-class {
color: red;
font-size: 20px;
}
`);

return <div className="dynamic-class">动态样式</div>;
}

新的服务端渲染架构

React 18 改进了 SSR 性能:

  1. 流式 SSR:服务器可以分块发送 HTML
  2. 选择性 Hydration:优先水化用户正在交互的部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { renderToPipeableStream } from "react-dom/server";

// 服务端
function handleRequest(req, res) {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.setHeader("Content-Type", "text/html");
pipe(res);
},
});
}

// 客户端
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);

Strict Mode 的变化

React 18 的严格模式会在开发环境中双重调用某些函数,帮助发现副作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { StrictMode } from "react";

// 在开发环境中,以下会被调用两次:
// - 组件函数体
// - useState、useMemo、useReducer 的初始化函数
// - useEffect、useLayoutEffect、useInsertionEffect 的 setup 函数

function App() {
return (
<StrictMode>
<MyComponent />
</StrictMode>
);
}

性能优化最佳实践

1. 合理使用 memo、useMemo、useCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 避免过度优化
// 错误:对简单组件使用 memo
const SimpleButton = memo(({ label }) => <button>{label}</button>);

// 正确:对复杂组件使用 memo
const ComplexList = memo(({ items }) => {
return (
<ul>
{items.map((item) => (
<ExpensiveItem key={item.id} data={item} />
))}
</ul>
);
});

// 正确:配合使用 useCallback
function Parent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
console.log("Clicked");
}, []);

return <ComplexList items={items} onClick={handleClick} />;
}

2. 代码分割和懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { lazy, Suspense } from "react";

// 路由级别分割
const Dashboard = lazy(() => import("./Dashboard"));
const Profile = lazy(() => import("./Profile"));

// 组件级别分割
const HeavyChart = lazy(() => import("./HeavyChart"));

function App() {
const [showChart, setShowChart] = useState(false);

return (
<div>
<button onClick={() => setShowChart(true)}>显示图表</button>
{showChart && (
<Suspense fallback={<div>加载中...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}

3. 虚拟化长列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { FixedSizeList } from "react-window";

function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);

return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}

4. 避免不必要的渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 使用 key 优化列表渲染
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
// 使用稳定的 ID 作为 key
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}

// 提取不变的内容到组件外
const STATIC_OPTIONS = [
{ value: "1", label: "选项 1" },
{ value: "2", label: "选项 2" },
];

function Select() {
return (
<select>
{STATIC_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}

5. 使用 Transitions 优化用户体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useState, useTransition } from "react";

function App() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("home");

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}

return (
<div>
<TabButton isActive={tab === "home"} onClick={() => selectTab("home")}>
首页
</TabButton>
<TabButton isActive={tab === "posts"} onClick={() => selectTab("posts")}>
文章 {isPending && "(加载中...)"}
</TabButton>

{tab === "home" && <HomePage />}
{tab === "posts" && <PostsPage />}
</div>
);
}

6. 图片优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用懒加载
function LazyImage({ src, alt }) {
return <img src={src} alt={alt} loading="lazy" decoding="async" />;
}

// 响应式图片
function ResponsiveImage({ src, alt }) {
return (
<picture>
<source media="(max-width: 600px)" srcSet={`${src}-small.jpg`} />
<source media="(max-width: 1200px)" srcSet={`${src}-medium.jpg`} />
<img src={`${src}-large.jpg`} alt={alt} />
</picture>
);
}

7. 状态管理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 Context 时避免不必要的重渲染
const StateContext = createContext();
const DispatchContext = createContext();

function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}

// 组件只订阅需要的部分
function Component() {
const dispatch = useContext(DispatchContext); // 不会因 state 变化而重渲染
const count = useContext(StateContext).count; // 只在 count 变化时重渲染

return <div onClick={() => dispatch({ type: "INCREMENT" })}>{count}</div>;
}

8. 使用 Web Workers 处理复杂计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useEffect, useState } from "react";

function useWorker(workerFn) {
const [result, setResult] = useState(null);

useEffect(() => {
const worker = new Worker(new URL("./worker.js", import.meta.url));

worker.onmessage = (e) => setResult(e.data);

return () => worker.terminate();
}, []);

return result;
}

// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};

总结

React 18 带来了并发渲染、自动批处理、Transitions 等强大特性,极大提升了应用性能和用户体验。在实际开发中,应该:

  1. 合理使用 Hooks:理解每个 Hook 的使用场景和最佳实践
  2. 性能优化:避免过早优化,先测量再优化
  3. 代码分割:使用 lazy 和 Suspense 减少初始加载时间
  4. 并发特性:利用 Transitions 和 useDeferredValue 优化用户体验
  5. 类型安全:使用 TypeScript 提高代码质量
  6. 测试:编写单元测试和集成测试确保代码可靠性

20260512 更新

react-compiler

官方中文文档:react-compiler

React-compiler是一个新的构建时工具,它可以自动优化你的 React 应用。在构建时分析你的组件代码,搞清楚「谁依赖谁」,然后在需要的地方自动插入 memoization,相当于帮你自动加 useMemo / useCallback / React.memo,不需要自己手写。

支持 react17/18/19

配置

配置文档:配置文档

1
2
3
4
5
6
7
8
9
10
11
// babel.config.js
module.exports = {
plugins: [
[
"babel-plugin-react-compiler",
{
// 编译选项
},
],
],
};

scheduler 调度器

Scheduler的核心目标是 ‌:在不阻塞浏览器主线程的前提下,按优先级有序推进任务执行 ‌,避免长任务导致页面掉帧。

  1. 双队列设计:内部维护两个任务队列,timerQueue 存放未到执行时间的延迟任务,taskQueue 存放已就绪的可执行任务,两个队列均基于最小堆实现,能快速取出优先级最高的任务。
  2. 优先级分级体系:内置 5 级任务优先级,对应不同的超时阈值:ImmediatePriority(-1ms)、UserBlockingPriority(250ms)、NormalPriority(5000ms)、LowPriority(10000ms)、IdlePriority(1073741823ms),优先级越高的任务越先被调度执行。
  3. 时间切片设计:默认单任务最大执行时长为 ‌5ms‌,通过 shouldYieldToHost 判断是否超时,超时后立即中断任务,把主线程归还给浏览器完成 UI 渲染。
  4. 宏任务驱动循环:基于 MessageChannel 实现宏任务调度,替代不稳定的 requestIdleCallback,保证任务调度的可控性和兼容性。

reconciler 协调器

Reconciler是 React 更新流程的中枢,基于 Fiber 架构实现可中断的调和过程,负责完成新旧 UI 的差异计算。

  1. fiber 工作单元模型:把每个组件节点转化为独立的 Fiber 节点,用 child/sibling/return 链表结构替代传统递归遍历,将整个更新过程拆分为一个个可暂停、可恢复的小工作单元。
  2. 双缓存树机制:同时维护 current(当前已渲染到屏幕的 Fiber 树)和 workInProgress(正在后台计算的新 Fiber 树)两棵树,通过 alternate 指针双向关联,计算过程完全不影响当前页面显示,中断后可直接丢弃未完成的临时树。
  3. 可中断的 Render 阶段 ‌:在内存中完成新旧 Fiber 树的 Diff 对比,为需要变更的节点打上 Placement/Update/Deletion 等副作用标记,整个过程是纯函数操作,不直接修改真实 DOM,支持被高优先级任务随时打断。
  4. 不可中断的 Commit 阶段 ‌:将 Render 阶段标记的所有副作用一次性同步提交到宿主环境(如 DOM),这个过程无法中断,保证 UI 最终状态与组件状态完全一致。

调度器与协调器的关系

Scheduler 负责从宏观层面分配主线程时间,决定“什么时候执行任务、什么时候让出主线程”;Reconciler 负责在调度器分配的时间片内,完成 Fiber 树的遍历和 Diff 计算,二者配合让 React 实现了并发渲染能力,既保证了复杂场景下的页面流畅度,又能让用户交互等高优先级任务得到及时响应。

Fiber

Fiber 的本质是一个 ‌ 普通的 JavaScript 对象 ‌,它是 React 内部用来表示组件节点、存储状态信息以及构建链表树结构的 ‌ 工作单元(Work Unit)‌。

五大核心属性

基础身份属性

  1. tag:标记 Fiber 的类型,如 HostRoot(根节点)、HostComponent(原生 DOM,如 div)、FunctionComponent(函数组件)、ClassComponent(类组件)等。React 根据 tag 决定如何处理该节点。
  2. key:用于 Diff 算法中识别兄弟节点的身份,帮助 React 判断节点是移动、新增还是删除。
  3. type:对应 JSX 中的元素类型。对于 DOM 节点是字符串(如 ‘div’),对于组件则是函数或类引用。

链表指针属性(核心)

这是 Fiber 能够替代递归栈、实现深度优先遍历(DFS)的关键。通过这三个指针,所有节点被串联成一个单向链表树。

  1. child‌:指向第一个子 Fiber 节点。
  2. sibling‌:指向下一个兄弟 Fiber 节点。
  3. return‌:指向父 Fiber 节点。当子节点处理完毕或需要回溯时,通过 return 指针返回父级

遍历逻辑示例 ‌:
React 从根节点开始,先处理 child;如果没有 child,则处理 sibling;如果也没有 sibling,则通过 return 回到父节点,继续寻找父节点的 sibling。这种结构使得遍历过程可以随时暂停(保存当前节点指针)和恢复。

双缓存与复用属性

为了实现“后台计算、前台显示”的双缓冲机制,Fiber 树有两棵:current 树(当前屏幕显示的)和 workInProgress 树(正在内存中计算的)。

  1. alternate‌:这是一个双向指针。current 树中的节点通过 alternate 指向 workInProgress 树中对应的节点,反之亦然。如果节点在更新中没有变化,React 会直接复用 alternate 指向的旧 Fiber 对象,避免重新创建,极大节省内存。
  2. pendingProps‌:即将应用的新 props。
  3. memoizedProps‌:上一次渲染完成并已经应用到 DOM 上的 props。Diff 阶段会对比这两个值来判断是否需要更新。

状态与实例属性

  1. stateNode‌:指向与该 Fiber 关联的真实实例。
  • 对于 HostComponent(如 div),它指向真实的 DOM 节点。
  • 对于 ClassComponent,它指向组件的类实例(this)。
  • 对于 FunctionComponent,通常为 null(因为函数组件没有实例)。
  1. memoizedState‌:
  • 对于类组件,存储当前的 state。
  • 对于函数组件,存储 Hooks 链表(如 useState, useEffect 的状态)。
  1. updateQueue‌:存储待处理的更新队列(如 setState 产生的更新对象),在渲染阶段会被处理并合并到 state 中。

副作用与优先级属性

这些属性用于指导 Commit 阶段如何更新 DOM,以及 Scheduler 如何调度任务。

  1. ‌flags (旧版本叫 effectTag)‌:用位运算标记当前节点需要执行的操作,如 Placement(插入)、Update(更新)、Deletion(删除)。
  2. subtreeFlags‌:汇总子树中所有节点的 flags,方便快速判断子树是否有副作用,无需遍历整个子树。
  3. lanes‌:表示当前更新的优先级模型(React 18+)。不同的事件(如点击、输入、定时器)对应不同的 lane,高优先级的 lane 可以打断低优先级的渲染。
  4. childLanes‌:子树中所有未处理更新的优先级汇总,用于判断是否需要重新渲染子树。

简化版 Fiber 结构代码示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function FiberNode(tag, key, pendingProps) {
// 1. 基础身份
this.tag = tag;
this.key = key;
this.type = null; // 组件类型或 DOM 标签名

// 2. 链表指针
this.return = null; // 父节点
this.child = null; // 第一个子节点
this.sibling = null; // 兄弟节点

// 3. 双缓存与状态
this.alternate = null; // 指向另一棵树中的对应节点
this.pendingProps = pendingProps; // 新 props
this.memoizedProps = null; // 旧 props
this.memoizedState = null; // 状态/Hooks
this.stateNode = null; // 真实 DOM 或组件实例

// 4. 副作用与调度
this.flags = 0; // 当前节点的副作用标记
this.subtreeFlags = 0; // 子树的副作用标记
this.lanes = 0; // 当前节点的更新优先级
this.childLanes = 0; // 子树的更新优先级
}

Fiber 结构的设计初衷就是为了 ‌ 把庞大的递归任务拆解成一个个小的、可中断的对象 ‌。

  1. 链表结构 ‌ (child/sibling/return) 让遍历不再依赖调用栈,可以随时暂停。
  2. 双缓存 ‌ (alternate) 让新旧树切换变得极快,且能复用节点。
  3. 优先级标记 ‌ (lanes/flags) 让 Scheduler 知道哪些任务紧急,哪些 DOM 操作需要执行。

react 性能优化

React 性能优化可以从减少不必要渲染、优化应用加载、利用新特性与工程手段这几个核心方向落地,覆盖从组件级到项目级的全链路优化方案。

减少不必要的组件渲染

  • 使用记忆化高阶组件 ‌:函数组件用 React.memo,类组件用 PureComponent 或自定义 shouldComponentUpdateprops 无变化时直接跳过重渲染。
  • 稳定引用类型 props‌:用 useMemo 缓存耗时派生数据,用 useCallback 缓存事件函数,避免父组件更新时子组件因引用变化触发重渲染。
  • 拆分大组件 ‌:将复杂大组件拆分为多个职责单一的小组件,配合 memo 精确控制更新范围,避免父组件更新导致整树重绘。
  • 合理使用 Context‌:拆分大体积 Context,避免单个 Context 更新触发所有消费者重渲染,传递稳定值给 memo 子组件。
  • 避免内联对象/匿名函数 ‌:渲染过程中不要直接创建内联对象、匿名函数作为 props,防止接收组件始终判定 props 变化而重渲染。

优化应用加载与列表性能

  • 列表使用唯一 key‌:用数据 ID 作为列表 key,禁止使用数组索引,提升节点复用率,减少删除、重排场景下的 DOM 操作开销。
  • 代码分割与懒加载 ‌:通过 React.lazy 和 Suspense 按需加载路由或非首屏组件,大幅减少首屏 JS 体积。
  • 开启包体积分析 ‌:用 webpack-bundle-analyzer 或 vite-bundle-analyzer 可视化依赖占比,优先优化体积过大的第三方库。
  • 避免请求瀑布流 ‌:多个独立异步请求用 Promise.all 并行发起,减少串行等待带来的网络延迟。
  • 优化第三方库导入 ‌:避免桶文件全量导入,直接引入所需子模块,或通过框架配置开启自动优化,提升冷启动速度。

利用新特性与工程优化

  • React 19 自动编译器 ‌:开启后自动完成记忆化处理,无需手动编写大量 useMemo/useCallback/memo 代码。
  • React 18 自动批处理 ‌:将短时间内多次状态更新合并为一次渲染,无论在事件回调、异步场景下都能统一减少渲染次数。
  • 生产环境构建 ‌:部署时使用 React 生产版本,开启 JS/CSS 代码压缩,禁用调试相关代码。
  • 延迟状态初始化 ‌:将耗时的状态初始化逻辑包装成回调函数,确保仅在组件首次挂载时执行一次。
  • 内存泄漏清理 ‌:组件卸载时及时清除定时器、事件监听器等副作用,避免内存占用持续升高拖慢应用。
  • 状态管理优化 ‌:避免过度嵌套状态,选择轻量化状态管理方案,减少不必要的数据传递和跨组件重渲染。
  • 性能瓶颈定位 ‌:使用 React Profiler 工具,精准定位渲染耗时过长的组件,查看触发组件更新的具体原因。

react18剖析
https://zouhualu.github.io/20250321/react18剖析/
作者
花鹿
发布于
2025年3月21日
许可协议