首页  |  学习总览  |  ← 上一章:工程化与构建 阶段五 · 第 5 章,共 10 章
阶段五 · 预计 120 小时 · 职业分水岭

框架核心 —— 组件化开发(React 主线)

框架是前端开发的主战场。本章以 React 为主线,从组件化思想讲到 Fiber 架构原理,每个概念都配代码。学完本章,你能画出「setState 后界面如何更新」的完整流程图,能读懂 React 源码的关键路径。

1组件化思想

一切从「为什么需要框架」开始。

1.1 原生 JS 开发的三座大山

  • 手动操作 DOM:数据变了要自己找元素、改属性,代码又长又容易漏。
  • 状态同步难:多个地方显示同一份数据,一处改了别处忘更新,bug 频出。
  • 代码复用差:相似 UI 只能复制粘贴,改一处要改多处。

React 的核心思路:「你只告诉我数据是什么样(状态),界面长什么样我自动算出来」。开发者从「操作 DOM」解放为「声明 UI」。这就是声明式编程——UI = f(state)。

// 声明式:UI 是状态的函数,状态变,UI 自动变
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击了 {count} 次
    </button>
  );
}

1.2 组件层级设计

页面
路由层
业务组件
绑定领域逻辑
通用组件
可复用 UI
基础组件
原子(按钮/输入框)
  • 单向数据流:数据从父组件流向子组件(props),子组件通过回调通知父级——方向清晰,好调试。
  • 组件单一职责:一个组件只做一件事,做大了就拆。
2JSX 与虚拟 DOM

JSX 是 React 的「魔法糖」,拆开看全是函数调用。

2.1 JSX 的本质:语法糖

// 你写的 JSX:
const el = <h1 className="title">你好</h1>;

// 编译后(babel/编译器转换):
const el = React.createElement("h1", { className: "title" }, "你好");

// createElement 返回一个普通对象(虚拟 DOM):
// { type: "h1", props: { className: "title", children: "你好" } }
💡 虚拟 DOM(Virtual DOM)是什么 一个描述 UI 的普通 JS 对象。每次状态变化,React 先算出「新的虚拟 DOM」,与旧的对比(diff),找出变化点,再批量更新真实 DOM。为什么不用直接操作真实 DOM?因为真实 DOM 操作昂贵(触发布局/绘制),而 JS 对象对比极快——「先在内存里比好,再去碰真实 DOM」。

2.2 JSX 高频语法

// 表达式:用 {} 包裹 JS 表达式
const name = "张三";
<p>你好,{name}</p>

// 条件渲染:三目运算符
{isLogin ? <LogoutButton /> : <LoginButton />}

// 列表渲染:map 必须有 key
{items.map(item => (
  <li key={item.id}>{item.name}</li>   // key 是 diff 的标识,别用 index 当 key
))}

// 事件:驼峰命名,传函数引用(不是调用)
<button onClick={handleClick}>点击</button>
⚠️ key 为什么不能用 index key 是 React 判断「这个元素是不是原来那个」的标识。用 index 当 key,列表增删时 React 会认错元素,导致状态错乱(比如输入框内容串位)。数据稳定唯一(如 id)当 key 才对。
3渲染流程:从 setState 到界面更新

本章最重要的一节,画得出这张图才算懂 React。

3.1 五步流程

① setState
触发更新
② 调度
安排优先级
③ 渲染
执行组件函数
④ 协调
diff 新旧虚拟 DOM
⑤ 提交
更新真实 DOM
  1. 触发:setState / useState 的 setter / 父组件重新渲染。
  2. 调度:React 把更新排队,按优先级(紧急/过渡)安排处理。
  3. 渲染:执行组件函数,重新生成虚拟 DOM 树。注意:渲染阶段不碰真实 DOM
  4. 协调:新虚拟 DOM 与旧树 diff,找出需要增删改的节点。
  5. 提交:把 diff 结果一次性应用到真实 DOM(可中断部分只在此阶段落盘)。

3.2 diff 算法核心(背下来)

React 的 diff 不是暴力对比整棵树,而是基于三个「合理假设」的启发式算法:

  • 同层对比:只对比同一层的节点,不跨层比较(跨层移动 = 先删后建,成本高)。
  • 同类型节点:type 相同则复用(只更新 props);type 不同直接重建(旧树整个扔掉)。
  • key 定位:列表通过 key 识别「谁移动了」,避免误删误建。
💡 三种对比结果的成本 type 不同 → 重建整棵子树(最贵);type 相同只改 props(便宜);key 变更 → React 认为元素变了,重建。所以:保持组件类型稳定、key 稳定,是性能的基本盘。
4状态与 Hooks(每日都用,必须吃透)

Hooks 是 React 16.8 之后的开发范式,用错最多也考得最多。

4.1 useState 与不可变更新

// 基本用法
const [count, setCount] = useState(0);   // 解构出 值 和 修改函数

// 修改对象/数组:永远用「新值」替换,不能直接改原对象!
const [user, setUser] = useState({ name: "张三", age: 25 });
setUser({ ...user, age: 26 });           // ✅ 展开运算符合成新对象
// user.age = 26; setUser(user); ❌ 直接改对象,React 无法感知变化

// 依赖上一次值:传函数形式
setCount(c => c + 1);                    // 多次调用都基于最新值
🚨 不可变更新的原因 React 靠「引用是否变化」判断状态是否改变。直接修改对象(地址不变)→ React 认为没变 → 不重新渲染。所以每次更新必须产生新引用。

4.2 useEffect:副作用处理(最容易写错)

// 副作用:数据请求、订阅、定时器、DOM 操作——不能直接写在渲染里
useEffect(() => {
  // 1. 组件挂载后执行
  fetchData();

  // 2. 返回清理函数:组件卸载前 或 依赖变化重新执行前 调用
  return () => clearTimer();
}, [deps]);   // 3. 依赖数组:依赖变了才重新执行

// 依赖数组的三种写法(必须背下来)
useEffect(fn);          // 每次渲染都执行(几乎没有场景该用)
useEffect(fn, []);      // 只在挂载时执行一次(模拟生命周期)
useEffect(fn, [a, b]);  // a 或 b 变化时执行(常用)
⚠️ 依赖数组的坑 依赖漏了 → 用的还是旧值(闭包陷阱);依赖写多了 → 无限循环。ESLint 的 exhaustive-deps 规则会提醒你补全依赖——别忽略它。若确实不想依赖某个值,用 ref 保存。

4.3 useMemo / useCallback:缓存

// useMemo:缓存计算结果(昂贵的计算 + 给子组件传稳定引用)
const sortedList = useMemo(() => {
  return list.sort((a, b) => a.value - b.value);  // 只有 list 变了才重算
}, [list]);

// useCallback:缓存函数引用(配合 memo 子组件使用)
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);   // id 不变,函数引用不变 → 子组件不用重新渲染
💡 为什么要缓存函数引用? 子组件用 React.memo 包裹后,只有 props 变化才重新渲染。如果父组件每次渲染都传「新函数」,子组件的 props 引用每次都变,memo 就失效了。useCallback 让函数引用稳定,memo 才有意义。

4.4 useRef:跨渲染的记忆

// useRef 返回一个对象 { current: 初始值 },它在整个生命周期保持不变,
// 修改 current 不触发重新渲染 —— 适合存「不需要渲染的值」
const timerRef = useRef(null);
timerRef.current = setTimeout(() => {}, 1000);

// 经典应用:拿 DOM 元素(如聚焦输入框)
const inputRef = useRef(null);
inputRef.current.focus();

4.5 Hooks 的两条铁律(为什么不能条件调用)

  1. 只在最顶层调用:不能放在 if、循环、嵌套函数里。
  2. 只在 React 函数组件/自定义 Hook 里调用
💡 原因(原理层面) Hooks 是「按调用顺序」存在组件对应的链(Fiber 的 memoizedState 链表)里的。React 靠「第几次调用」找到对应的 Hook 状态。如果条件语句改变了调用顺序,React 就会把状态对错号——这是 React 从设计上禁止的原因,不是教条。

4.6 自定义 Hook:逻辑复用的终极方案

// 自定义 Hook = 以 use 开头的函数,内部可调用其他 Hooks
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initialValue;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}

// 使用:任何组件都能复用这套逻辑
const [theme, setTheme] = useLocalStorage("theme", "light");
5组件通信与 Context

组件间数据怎么流动?一层层传还是走捷径?

通信场景方式
父 → 子props 传值(最常用、最清晰)
子 → 父父传回调函数,子调用并传参
兄弟之间状态提升到共同父组件,通过父组件中转
深层跨级(祖 → 孙)Context(避免 props 层层透传)
无关组件(全局)全局状态库(阶段六详讲)

5.1 Context 的用法与陷阱

// 1. 创建 Context
const ThemeContext = createContext("light");

// 2. Provider 提供值(放组件树顶层)
<ThemeContext.Provider value={theme}>
  <App />
</ThemeContext.Provider>

// 3. 消费:useContext 钩子
function Button() {
  const theme = useContext(ThemeContext);   // 拿到最近的 Provider 的值
  return <button className={theme}>按钮</button>;
}
⚠️ Context 的性能陷阱 Provider 的 value 一变,所有消费该 Context 的组件都会重新渲染(无论它们关心的是不是变化的那部分)。优化:① 把 value 用 useMemo 缓存;② 拆成多个小 Context;③ 只在真正需要跨层的场景用它。
6组件性能优化

框架层的性能优化,核心就一件事:减少无效渲染。

6.1 memo:跳过重复渲染

// React.memo:props 没变就跳过渲染(浅比较)
const ExpensiveList = React.memo(function List({ items }) {
  // 只有 items 引用变了才重新渲染
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});

// 注意:配合 useCallback 使用才有效(见 4.3)
// 注意:只做浅比较,嵌套对象变化可能感知不到 → 用 useMemo 保证引用稳定

6.2 常见优化手段一览

手段解决什么
React.memo子组件因父组件渲染而「躺枪」重渲染
useMemo / useCallback计算结果/函数引用不稳定导致 memo 失效
代码分割(React.lazy + Suspense)首屏体积过大
虚拟列表(react-window)渲染上千条列表项卡顿
useTransition / useDeferredValue非紧急更新抢占主线程,输入卡顿
错误边界(ErrorBoundary)一个组件崩溃拖垮整页
// 代码分割:路由级懒加载
const Home = React.lazy(() => import("./pages/Home"));
<Suspense fallback={<Loading />}>
  <Home />
</Suspense>

// 错误边界:class 组件才能做(Hooks 暂不支持)
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  render() {
    return this.state.hasError ? <h1>出错了</h1> : this.props.children;
  }
}
7React 18 / 19 新特性

框架在进化,掌握新特性是资深工程师的标配。

7.1 并发渲染:可中断的渲染

  • React 18 的核心是「并发特性」:渲染可以被更高优先级的工作打断,保证用户交互始终流畅。
  • 渲染不再是「一口气做完」,而是分片、可暂停、可恢复。
// useTransition:把非紧急更新标记为「过渡」,可被紧急更新打断
const [isPending, startTransition] = useTransition();
startTransition(() => {
  setFilterText(newText);   // 大列表过滤这种重活,让给空闲时间做
});

// useDeferredValue:延迟某个值的更新
const deferredQuery = useDeferredValue(query);  // 输入不卡,结果稍后跟上

7.2 Suspense 与 Server Components

  • Suspense:数据加载的「等待态」声明式处理——渲染遇到异步依赖时显示 fallback。
  • Server Components(RSC):组件可以在服务器上渲染再发给浏览器,减少客户端 JS 体积、直接访问数据库。这是 React 19 时代的大方向——「把计算留在服务器,浏览器只拿结果」。
💡 面试加分:RSC 解决什么 传统 SPA 首屏要下载全部 JS 才能渲染;RSC 让部分组件在服务器渲染成 HTML 流式下发,浏览器几乎不加载对应 JS,首屏大幅提速。同时服务器组件可以直接读数据库,不用再单独写接口。
8React 源码导读:Fiber 架构

能看懂这一节,你已经超越大多数「会用」的开发者。

8.1 为什么需要 Fiber:从「一次干完」到「分片干活」

旧版 React 的协调(diff)是递归的、一次做完的——组件树很大时,这一口气会卡住主线程几百毫秒,用户输入没反应。Fiber 的诞生就是为了解决这个:把渲染工作拆成一个个小任务(Fiber 节点),能随时暂停、让位给用户交互,再继续。

  • Fiber 是什么:一个描述组件的普通对象,包含类型、props、state、父/子/兄弟指针——它把树变成链表,链表可以遍历中断、恢复。
  • 双缓冲:同时存在 current(已提交的树)和 workInProgress(正在构建的树),构建完成一次性切换——用户永远看到完整的 UI,不会看到半渲染状态。

8.2 两大阶段:渲染阶段与提交阶段

渲染阶段(可中断)提交阶段(不可中断)
干什么执行组件、diff、构建 workInProgress 树把结果应用到真实 DOM、执行副作用
能否中断✅ 能(分片 + 让位)❌ 必须一口气完成
常见误区副作用(请求/订阅)不该放这(会被执行多次)useEffect 在这里执行
⚠️ 为什么副作用不能放渲染阶段 渲染阶段可中断、可重复执行,如果在里面发请求,可能发好几次。所以 useEffect 放提交阶段、在 DOM 更新后执行——这也是它被称为「副作用」钩子的原因。

8.3 学习路径建议

读源码别从头读到尾,按「问题驱动」读关键路径:

  1. useState 的实现出发(Hooks 链表怎么存储)
  2. setState 出发(调度怎么排队、优先级怎么分)
  3. 从「首次渲染」出发(createRoot → render → 挂载到真实 DOM)
  4. 从「更新渲染」出发(重新执行组件 → diff → 提交)
✅ 读源码的正确姿势 先读解析类文章建立地图 → 再带着问题进源码 → 每个关键函数写「它做了什么、被谁调用」的笔记 → 最后自己画调用链图。不要逐行读,抓主干。
9备选视角:Vue 3 的响应式原理

面试常问框架对比,理解 Vue 的响应式能让你对「框架差异」有立体认知。

9.1 Vue 3 响应式:Proxy + 依赖收集

// Vue 3 用 Proxy 代理对象,读取时收集依赖,修改时触发更新
const state = reactive({ count: 0 });

// 原理(简化版):
// 读取 state.count → 把当前副作用(渲染函数)记入依赖表
// 修改 state.count → 找到依赖表里的副作用 → 重新执行(更新界面)

// 对比 React:React 是「手动指定」——useState 返回 setter,调用就更新;
// Vue 是「自动追踪」——数据变了,用到它的地方自动更新。
对比维度ReactVue 3
更新粒度组件级(配合 memo 细化)细粒度依赖追踪(精确到变量)
模板JSX(JS 全部能力)模板语法(+ JSX 支持)
学习曲线概念多但自由度高上手快,约定多
生态最大,社区最活跃中文社区强,生态完善
💡 面试回答框架对比的姿势 别说「React 更好/Vue 更好」,说「它们的设计哲学不同」:React 是「UI = f(state)」的纯函数思路,Vue 是「状态代理自动更新」的响应式思路。选择取决于团队与项目。
10实战项目(通关必做)

本章项目最多也最重,但做完你就真正「会框架」了。

项目一:用 React 重写 Todo 应用

要求:① 组件拆分合理(App / TodoList / TodoItem / AddTodo / Filter);② 状态提升到 App 统一管理;③ 自定义 Hook 封装 localStorage 持久化;④ 列表用稳定 id 做 key;⑤ 实现过滤功能。

项目二:实现一个虚拟滚动表格

渲染 10 万行数据不卡顿。核心思路:只渲染可视区 + 上下缓冲区的行,滚动时计算偏移。用 useMemo 优化计算、memo 优化行组件。完成后用 React DevTools 观察渲染次数。

项目三:封装一个自定义 Hook 库

至少实现:useLocalStorage、useDebounce(防抖值)、useClickOutside(点击外部关闭)、useToggle、useFetch(带取消与 loading 状态)。发布到你自己的 npm 包(可选)。

项目四:画「setState 后发生了什么」的流程图

从点击事件 → setState → 调度 → 渲染 → diff → 提交 → DOM 更新 → useEffect 执行,画出完整链路并标注每步可否中断。这张图是你面试 React 岗位的底气。

11通关自检题

本章是面试重灾区,每题都值得反复咀嚼。

1. 虚拟 DOM 是什么?为什么比直接操作 DOM 快?

2. JSX 编译后是什么?createElement 返回什么?

3. 列表的 key 为什么不能用 index?

4. diff 算法的三个假设是什么?type 不同会怎样?

5. 为什么更新对象状态必须用展开运算符合成新对象?

6. useEffect 的三种依赖写法?清理函数什么时候执行?

7. useMemo 和 useCallback 的区别?为什么它们能让 memo 生效?

8. 为什么 Hooks 不能写在条件语句里?(要答到原理层面)

9. Context 的 value 变化会怎样?怎么优化?

10. React.memo 解决什么问题?它有什么局限?

11. 并发特性是什么?useTransition 解决什么问题?

12. Fiber 是什么?为什么它能中断渲染?双缓冲是什么?

13. 为什么副作用不能放渲染阶段?useEffect 在哪执行?

14. Vue 3 的响应式原理?和 React 的更新机制有什么本质不同?

✅ 全部通关? 你已经具备框架级开发能力。下一阶段《状态管理与数据流》将解决「多个组件共享数据怎么管」的问题,这是从「写组件」到「设计应用」的过渡。