框架是前端开发的主战场。本章以 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 岗位的底气。
本章是面试重灾区,每题都值得反复咀嚼。