框架是前端开发的主战场。本章以 React 为主线,从组件化思想讲到 Fiber 架构原理,每个概念都配代码。学完本章,你能画出「setState 后界面如何更新」的完整流程图,能读懂 React 源码的关键路径。
一切从「为什么需要框架」开始。
React 的核心思路:「你只告诉我数据是什么样(状态),界面长什么样我自动算出来」。开发者从「操作 DOM」解放为「声明 UI」。这就是声明式编程——UI = f(state)。
// 声明式:UI 是状态的函数,状态变,UI 自动变
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
JSX 是 React 的「魔法糖」,拆开看全是函数调用。
// 你写的 JSX:
const el = <h1 className="title">你好</h1>;
// 编译后(babel/编译器转换):
const el = React.createElement("h1", { className: "title" }, "你好");
// createElement 返回一个普通对象(虚拟 DOM):
// { type: "h1", props: { className: "title", children: "你好" } }
// 表达式:用 {} 包裹 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>
本章最重要的一节,画得出这张图才算懂 React。
React 的 diff 不是暴力对比整棵树,而是基于三个「合理假设」的启发式算法:
Hooks 是 React 16.8 之后的开发范式,用错最多也考得最多。
// 基本用法
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); // 多次调用都基于最新值
// 副作用:数据请求、订阅、定时器、DOM 操作——不能直接写在渲染里
useEffect(() => {
// 1. 组件挂载后执行
fetchData();
// 2. 返回清理函数:组件卸载前 或 依赖变化重新执行前 调用
return () => clearTimer();
}, [deps]); // 3. 依赖数组:依赖变了才重新执行
// 依赖数组的三种写法(必须背下来)
useEffect(fn); // 每次渲染都执行(几乎没有场景该用)
useEffect(fn, []); // 只在挂载时执行一次(模拟生命周期)
useEffect(fn, [a, b]); // a 或 b 变化时执行(常用)
exhaustive-deps 规则会提醒你补全依赖——别忽略它。若确实不想依赖某个值,用 ref 保存。
// 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 才有意义。
// useRef 返回一个对象 { current: 初始值 },它在整个生命周期保持不变,
// 修改 current 不触发重新渲染 —— 适合存「不需要渲染的值」
const timerRef = useRef(null);
timerRef.current = setTimeout(() => {}, 1000);
// 经典应用:拿 DOM 元素(如聚焦输入框)
const inputRef = useRef(null);
inputRef.current.focus();
// 自定义 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");
组件间数据怎么流动?一层层传还是走捷径?
| 通信场景 | 方式 |
|---|---|
| 父 → 子 | props 传值(最常用、最清晰) |
| 子 → 父 | 父传回调函数,子调用并传参 |
| 兄弟之间 | 状态提升到共同父组件,通过父组件中转 |
| 深层跨级(祖 → 孙) | Context(避免 props 层层透传) |
| 无关组件(全局) | 全局状态库(阶段六详讲) |
// 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>;
}
框架层的性能优化,核心就一件事:减少无效渲染。
// 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 保证引用稳定
| 手段 | 解决什么 |
|---|---|
| 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;
}
}
框架在进化,掌握新特性是资深工程师的标配。
// useTransition:把非紧急更新标记为「过渡」,可被紧急更新打断
const [isPending, startTransition] = useTransition();
startTransition(() => {
setFilterText(newText); // 大列表过滤这种重活,让给空闲时间做
});
// useDeferredValue:延迟某个值的更新
const deferredQuery = useDeferredValue(query); // 输入不卡,结果稍后跟上
能看懂这一节,你已经超越大多数「会用」的开发者。
旧版 React 的协调(diff)是递归的、一次做完的——组件树很大时,这一口气会卡住主线程几百毫秒,用户输入没反应。Fiber 的诞生就是为了解决这个:把渲染工作拆成一个个小任务(Fiber 节点),能随时暂停、让位给用户交互,再继续。
| 渲染阶段(可中断) | 提交阶段(不可中断) | |
|---|---|---|
| 干什么 | 执行组件、diff、构建 workInProgress 树 | 把结果应用到真实 DOM、执行副作用 |
| 能否中断 | ✅ 能(分片 + 让位) | ❌ 必须一口气完成 |
| 常见误区 | 副作用(请求/订阅)不该放这(会被执行多次) | useEffect 在这里执行 |
读源码别从头读到尾,按「问题驱动」读关键路径:
useState 的实现出发(Hooks 链表怎么存储)setState 出发(调度怎么排队、优先级怎么分)面试常问框架对比,理解 Vue 的响应式能让你对「框架差异」有立体认知。
// Vue 3 用 Proxy 代理对象,读取时收集依赖,修改时触发更新
const state = reactive({ count: 0 });
// 原理(简化版):
// 读取 state.count → 把当前副作用(渲染函数)记入依赖表
// 修改 state.count → 找到依赖表里的副作用 → 重新执行(更新界面)
// 对比 React:React 是「手动指定」——useState 返回 setter,调用就更新;
// Vue 是「自动追踪」——数据变了,用到它的地方自动更新。
| 对比维度 | React | Vue 3 |
|---|---|---|
| 更新粒度 | 组件级(配合 memo 细化) | 细粒度依赖追踪(精确到变量) |
| 模板 | JSX(JS 全部能力) | 模板语法(+ JSX 支持) |
| 学习曲线 | 概念多但自由度高 | 上手快,约定多 |
| 生态 | 最大,社区最活跃 | 中文社区强,生态完善 |
本章项目最多也最重,但做完你就真正「会框架」了。
要求:① 组件拆分合理(App / TodoList / TodoItem / AddTodo / Filter);② 状态提升到 App 统一管理;③ 自定义 Hook 封装 localStorage 持久化;④ 列表用稳定 id 做 key;⑤ 实现过滤功能。
渲染 10 万行数据不卡顿。核心思路:只渲染可视区 + 上下缓冲区的行,滚动时计算偏移。用 useMemo 优化计算、memo 优化行组件。完成后用 React DevTools 观察渲染次数。
至少实现:useLocalStorage、useDebounce(防抖值)、useClickOutside(点击外部关闭)、useToggle、useFetch(带取消与 loading 状态)。发布到你自己的 npm 包(可选)。
从点击事件 → setState → 调度 → 渲染 → diff → 提交 → DOM 更新 → useEffect 执行,画出完整链路并标注每步可否中断。这张图是你面试 React 岗位的底气。
本章是面试重灾区,每题都值得反复咀嚼。
答案:虚拟 DOM 是描述 UI 的普通 JS 对象,包含 type、props、children 等信息。速度优势来自批量更新策略:先在内存完成新旧虚拟 DOM 对比(diff),再将变化批量应用到真实 DOM,避免了每次状态变化直接触发昂贵的 DOM API 调用(触发布局/重绘/回流)。
答案:JSX 编译后是 React.createElement(type, props, ...children) 调用。createElement 返回一个普通 JS 对象(React元素/虚拟 DOM),结构为:{ type: "div"|组件, props: { ... }, children: [ ... ], key, ref }。
答案:key 是 React diff 时判断「元素身份」的标识。用 index 作为 key,当列表发生增删(尤其是头部插入/删除)时,key 值与实际数据的对应关系会错位,导致:① 组件状态串位(如 checkbox 勾选项乱);② 非必要的重建或错误的复用(传了旧值给新数据)。
答案:三条启发式规则:① 同层对比 — 只比较同一层级节点,跨层移动视为「先删后建」;② 类型判断 — 相同 type 复用、只更新 props,type 不同销毁整棵子树重建;③ key 定位 — 列表通过 key 判断增删移动,避免误删。type 不同时旧子树全部销毁(包括其内部 state 丢失),以新组件替代。
{isA ? <A /> : <B />},A 和 B 切换会导致状态丢失。解决:保持类型稳定(如始终渲染同一组件、用 display 控制显隐),或用不同 key 让 React 正确创建新实例。答案:React 使用 Object.is(引用比较)判断状态是否变化。直接修改原对象(如 obj.x = 1)引用地址不变 → React 认为没变化 → 不触发重渲染。展开运算符 / Object.assign / immer 都能产生新引用,React 能正确感知并触发更新。
答案:三种写法 — ① 省略依赖:每次渲染后都执行(极少使用);② []:仅挂载时执行一次(类似 componentDidMount);③ [a, b]:a 或 b 变化时重新执行。清理函数执行时机:组件卸载前、以及依赖变化导致 effect 重新执行前(先清理旧的、再跑新的)。
答案:useMemo 缓存「计算结果」(值),useCallback 缓存「函数引用」。本质 useMemo(fn, deps) 等价于 useCallback(fn, deps)。React.memo 对 props 做浅比较,若父组件每次传新函数或新对象,memo 永远失效 — 用 useCallback/useMemo 让引用稳定,memo 才能正确跳过。
答案:React 内部为每个组件维护一个 Fiber 节点,其中 memoizedState 以链表形式按调用顺序依次存储每个 Hook 的状态。React 靠「第几次 Hook 调用」对应「链表中第几个节点」来读取状态。如果条件语句导致某次渲染 Hook 调用顺序/数量改变,链表索引错位 → 状态串号读取错误。
答案:Provider value 改变时,所有使用 useContext 消费该 Context 的组件全部重新渲染,即使它们关心的只是没变的那部分数据。优化方案:① useMemo 稳定 value 引用,避免每次渲染都产生新对象;② 拆分多个细粒度 Context;③ 子组件用 React.memo 包裹,并用选择器模式消费。
答案:解决「父组件重渲染导致 props 未变的子组件也连带渲染」的问题(memo 返回相同 props 时跳过渲染)。局限:① 只做浅比较(shallow equal),对象/数组内部变化感知不到;② 若配合的 props 引用不稳定(新对象/新函数),memo 形同虚设;③ memo 本身有比对开销 — 组件很轻量时带去得不偿失;④ 可用第二个参数 areEqual 自定义比较逻辑。
答案:并发特性是 React 18 引入的「可中断渲染」机制:渲染过程可被打断,让高优先级工作(用户输入/点击)插队执行。useTransition 把非紧急的状态更新(如大列表搜索结果、路由切换)标记为「过渡更新」,可被紧急更新打断优先执行,避免输入卡顿。
答案:Fiber 是描述组件的普通对象(含 type、props、state、父/子/兄弟指针),把树转为链表后遍历可随时中断恢复。双缓冲:同时存在 current(已提交的树)和 workInProgress(正在构建的树),构建完成一次性切换 — 用户永远看到完整 UI。
答案:渲染阶段可中断、可重复执行,如果把副作用(订阅/数据请求/操作 DOM)放里面,可能执行多次导致逻辑错误或数据不一致。useEffect 执行时机:提交阶段 DOM 更新完成后、浏览器绘制后(异步、不阻塞绘制)。布局 effect(useLayoutEffect)在 DOM 更新后同步执行。
答案:Vue 3 用 Proxy 代理对象,读取属性时自动收集依赖(effect → target 映射),属性变化时精确触发所有依赖重新执行 — 细粒度、自动追踪。本质差异:React 是「手动指定更新」(调 setter 触发),Vue 是「自动依赖追踪」(数据变了用到它的地方自动更新)。