首页  |  学习总览  |  ← 上一章:工程化与构建 阶段五 · 第 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 快?

答案:虚拟 DOM 是描述 UI 的普通 JS 对象,包含 type、props、children 等信息。速度优势来自批量更新策略:先在内存完成新旧虚拟 DOM 对比(diff),再将变化批量应用到真实 DOM,避免了每次状态变化直接触发昂贵的 DOM API 调用(触发布局/重绘/回流)。

💡 深度解析「虚拟 DOM 快」并不完全等价于「虚拟 DOM 比真实 DOM 操作快」—— 任何 DOM 操作本身都是昂贵的,所以核心是「批量 diff + 批量更新」的策略优势。Svelte 没有虚拟 DOM 也能极快,靠的是编译期静态分析直接生成操作 DOM 的代码。React 选择虚拟 DOM 是为了「用JS描述UI」的声明式开发体验。

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

答案:JSX 编译后是 React.createElement(type, props, ...children) 调用。createElement 返回一个普通 JS 对象(React元素/虚拟 DOM),结构为:{ type: "div"|组件, props: { ... }, children: [ ... ], key, ref }。

💡 理解要点type 可以是字符串(原生标签如 "div")或函数/类(自定义组件)。createElement 本身不做 DOM 操作,只是生成描述对象。真正渲染是 React 遍历这些对象创建/更新 Fiber 节点、最终操作真实 DOM。

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

答案:key 是 React diff 时判断「元素身份」的标识。用 index 作为 key,当列表发生增删(尤其是头部插入/删除)时,key 值与实际数据的对应关系会错位,导致:① 组件状态串位(如 checkbox 勾选项乱);② 非必要的重建或错误的复用(传了旧值给新数据)。

⚠️ 反例验证在可输入的列表中用 index 做 key,在头部插入一行 → 原来第 1 行的输入框内容会「跑到」第 2 行。因为 key=0 的输入框还是 React 中那个输入框,但数据已变成新插入的那条。用稳定 id 则元素跟随数据正确移动。

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

答案:三条启发式规则:① 同层对比 — 只比较同一层级节点,跨层移动视为「先删后建」;② 类型判断 — 相同 type 复用、只更新 props,type 不同销毁整棵子树重建;③ key 定位 — 列表通过 key 判断增删移动,避免误删。type 不同时旧子树全部销毁(包括其内部 state 丢失),以新组件替代。

💡 面试加分当条件渲染导致同位置 type 改变时:{isA ? <A /> : <B />},A 和 B 切换会导致状态丢失。解决:保持类型稳定(如始终渲染同一组件、用 display 控制显隐),或用不同 key 让 React 正确创建新实例。

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

答案:React 使用 Object.is(引用比较)判断状态是否变化。直接修改原对象(如 obj.x = 1)引用地址不变 → React 认为没变化 → 不触发重渲染。展开运算符 / Object.assign / immer 都能产生新引用,React 能正确感知并触发更新。

💡 本质理解这不是 React 的特殊要求,而是「不可变数据」范式的核心:更新产生新值而非修改旧值。好处:便于时间旅行调试、简化变化检测、避免副作用。复杂嵌套更新推荐 immer.js 库简化写法。

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

答案:三种写法 — ① 省略依赖:每次渲染后都执行(极少使用);② []:仅挂载时执行一次(类似 componentDidMount);③ [a, b]:a 或 b 变化时重新执行。清理函数执行时机:组件卸载前、以及依赖变化导致 effect 重新执行前(先清理旧的、再跑新的)。

⚠️ 高频易错依赖数组遗漏会导致闭包捕获过期值;把对象/数组直接放入依赖会每次重新渲染(引用变化)。解决方案:完整填写依赖(ESLint exhaustive-deps 帮助检测),或使用 useMemo/useCallback 稳定引用,或使用 ref 保存不需要触发 effect 的「最新值」。

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

答案:useMemo 缓存「计算结果」(值),useCallback 缓存「函数引用」。本质 useMemo(fn, deps) 等价于 useCallback(fn, deps)。React.memo 对 props 做浅比较,若父组件每次传新函数或新对象,memo 永远失效 — 用 useCallback/useMemo 让引用稳定,memo 才能正确跳过。

💡 使用时机缓存是有成本的(内存占用+比较开销)。不要滥用 — 只对以下场景使用:① 子组件用 React.memo 且传了函数/对象props;② 消费值作为其他 Hook 依赖(避免无限循环);③ 计算确实昂贵(大列表排序、复杂数据转换)。普通计算直接算即可。

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

答案:React 内部为每个组件维护一个 Fiber 节点,其中 memoizedState 以链表形式按调用顺序依次存储每个 Hook 的状态。React 靠「第几次 Hook 调用」对应「链表中第几个节点」来读取状态。如果条件语句导致某次渲染 Hook 调用顺序/数量改变,链表索引错位 → 状态串号读取错误。

💡 原理图解Fiber.memoizedState → Hook1 → Hook2 → Hook3 → null。每次渲染从头开始遍历,第 1 个 useState 对应 Hook1,第 2 个 useState 对应 Hook2。若把 Hook2 放在 if(false) 中跳过 → 第 2 次 useState 就会读到 Hook3 的数据。这就是 Hook 规则的本质约束。

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

答案:Provider value 改变时,所有使用 useContext 消费该 Context 的组件全部重新渲染,即使它们关心的只是没变的那部分数据。优化方案:① useMemo 稳定 value 引用,避免每次渲染都产生新对象;② 拆分多个细粒度 Context;③ 子组件用 React.memo 包裹,并用选择器模式消费。

💡 设计建议Context 是「跨层传值工具」,不是「状态管理库」。频繁变化的全局状态推荐用 Zustand / Jotai / Valtio 等更专业的库,它们能实现精准订阅、避免无关重渲染。Context 适合低频次变更的数据(主题、用户信息、语言)。

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

答案:解决「父组件重渲染导致 props 未变的子组件也连带渲染」的问题(memo 返回相同 props 时跳过渲染)。局限:① 只做浅比较(shallow equal),对象/数组内部变化感知不到;② 若配合的 props 引用不稳定(新对象/新函数),memo 形同虚设;③ memo 本身有比对开销 — 组件很轻量时带去得不偿失;④ 可用第二个参数 areEqual 自定义比较逻辑。

💡 实践原则默认不要 memo 所有内容!当出现实际性能瓶颈时(React Profiler 定位到某子组件频繁重渲染)再针对性地 memo。永远记住:memo + useCallback + useMemo 是三件套,单独的 memo 无意义。

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

答案:并发特性是 React 18 引入的「可中断渲染」机制:渲染过程可被打断,让高优先级工作(用户输入/点击)插队执行。useTransition 把非紧急的状态更新(如大列表搜索结果、路由切换)标记为「过渡更新」,可被紧急更新打断优先执行,避免输入卡顿。

💡 区分使用场景useTransition:手动标记非紧急的状态更新。useDeferredValue:被动延迟某个值的更新(如第三方库传来的值)。典型场景:搜索输入框 — 输入即时响应(紧急),搜索结果可稍后跟上(非紧急)。

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

答案:Fiber 是描述组件的普通对象(含 type、props、state、父/子/兄弟指针),把树转为链表后遍历可随时中断恢复。双缓冲:同时存在 current(已提交的树)和 workInProgress(正在构建的树),构建完成一次性切换 — 用户永远看到完整 UI。

💡 核心理解Fiber 是工作单元(work unit),不是「纤程/协程」的 Fiber。每个组件对应一个 Fiber 节点,调度器按优先级遍历、处理、暂停、恢复遍历。双缓冲类似图形学的前后缓冲策略:在后台画好一整帧再一次性替换,避免用户看到中间状态。

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

答案:渲染阶段可中断、可重复执行,如果把副作用(订阅/数据请求/操作 DOM)放里面,可能执行多次导致逻辑错误或数据不一致。useEffect 执行时机:提交阶段 DOM 更新完成后、浏览器绘制后(异步、不阻塞绘制)。布局 effect(useLayoutEffect)在 DOM 更新后同步执行。

⚠️ 面试细节useEffect 是异步的(不阻塞浏览器绘制),useLayoutEffect 是同步的(在浏览器绘制前执行,适合读取 DOM 布局信息后同步调整)。React 18 严格模式下开发环境 useEffect 会故意「挂载→卸载→重挂载」帮助发现副作用清理遗漏。

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

答案:Vue 3 用 Proxy 代理对象,读取属性时自动收集依赖(effect → target 映射),属性变化时精确触发所有依赖重新执行 — 细粒度、自动追踪。本质差异:React 是「手动指定更新」(调 setter 触发),Vue 是「自动依赖追踪」(数据变了用到它的地方自动更新)。

💡 面试加分回答两者各有取舍:React 的「手动指定」更显式、易调试(多了心智模型成本);Vue 的「自动追踪」更省心(多了运行时开销,Proxy 兼容性问题)。理解差异比评价好坏更重要 — 优秀工程师根据场景选择合适的范式。
✅ 全部通关? 你已经具备框架级开发能力。下一阶段《状态管理与数据流》将解决「多个组件共享数据怎么管」的问题,这是从「写组件」到「设计应用」的过渡。