单页应用与 React 的四个核心想法
单页应用与 React 的四个核心想法
这一章讲什么: 为什么今天的 Web 应用要建成「单页应用(SPA)」, React 是靠哪四个想法立足的,以及一个 React 项目从脚手架到跑起来长什么样。 读完你会理解 React 世界里最重要的一个区分:「重新渲染」和「改页面」根本不是一回事。
1. 先讲历史:网页本来是一页一页的文件
最早的 Web 就是静态 HTML 文件:输入网址,服务器还你一个文件1。 后来人们想在服务器上动态拼页面,于是有了 CGI 和服务器端渲染(SSR)—— 浏览器每发一次请求,服务器现算一份完整的 HTML 寄回来2。 不管静态还是 SSR,模式都一样:以「页」为单位,任何像样的内容变化都要整页重载3。
问题出在对照组。同一时期的原生应用(桌面、手机)是另一种体验: 整个应用装在设备上,打开即用,画什么界面都在本地代码里,不需要为「换屏」 惊动服务器——只是为「取数据」才联网4。所以原生应用感觉明显更快、更跟手。
SPA 就是把这个「原生体验」搬到浏览器里:应用全下载下来在客户端跑, 不再一页一页等服务器5。这本书要建的正是这种应用。
2. SPA 的四张名片
原书列了单页应用共有的四个特征,无论用 React、Angular 还是 Vue 都成立6:
| 特征 | 意思 |
|---|---|
| 单张 HTML 页面 | 整个应用只有一张 HTML 页面,永远只加载它一次;「换屏」不再是「换文件」 |
| 客户端渲染 | 那张 HTML 几乎是空的,界面由 JavaScript 在用户的浏览器里画出来 |
| 预加载资源 | 脚本随主 HTML 一起下载(资源指 JS/CSS/图片等文件)(现代做法会按需切分,非关键的等用到再加载) |
| 虚拟路由 | URL 不再指向服务器上的真实文件,只是应用内部的「逻辑门牌」 |
虚拟路由值得多讲一句,它是 SPA 和经典 Web 最直观的分界。经典路由里, URL 一敲回车,请求就发回服务器;虚拟路由里,URL 指的服务器路径根本不存在, 回发永远不会发生——应用把 URL 当作内部不同「屏」之间的逻辑分隔, 某些 URL 出现时就触发某些行为7。你在地址栏里看到「跳转」, 全程没有离开过浏览器。
代价也如实交代:SPA 的组件和库多,构建更复杂——只是现代工具 (Vite 这类)已经把这层开销压得够低8。
3. React 的四个核心想法
浏览器拿到 HTML 后,会把它解析(把文本逐层读成结构)成一棵逻辑树,叫 DOM(文档对象模型)—— 页面上所有元素、它们的顺序和嵌套关系都在树上9。所有网站都有 DOM; React 的独特之处在于围绕它建立的四个想法10:
① 组件 UI 拆成自包含、可复用的零件,从小到大拼装
② 声明式渲染 你只说「界面应该长什么样」,怎么改 DOM 由 React 决定
③ 虚拟 DOM React 在内存里另养一棵树;状态变了就重建、比差异、只改该改的
④ state/props 组件自己的会变数据叫 state;父组件传下来的叫 props;
两者一变,React 重新调用组件函数
图说:①是分工,②是风格,③是手段,④是扳机。四件事合起来 = React。想法②是 React 的定义性特征。 老写法(jQuery 那代)是命令式: 你一步一步告诉浏览器「删掉这个节点、加上那个节点」。React 是声明式: 你声明「状态是 X 时界面长这样」,剩下的事它办11。
想法③有一个广为流传的误解,原书专门纠正:虚拟 DOM 的价值不是让 React 比直接操作 DOM 更快——纯性能角度它往往还略慢一点;它的真正价值是 撑起声明式的编程模型,并且把「渲染目标」抽象掉了(所以同一套 React 还能驱动 React Native 这样的非浏览器界面)12。机制上:状态变化时,React 在内存里 建一棵新的虚拟树,和旧的比差异(render phase,渲染阶段), 再把必要的改动应用到真 DOM 上(commit phase,提交阶段)—— 比较全程发生在 React 的两棵内存树之间,根本不碰浏览器 DOM13。
想法④是全书后面所有章节的发动机。 组件有自己的本地状态 state,
也从父组件收 props;任何一个变化,React 就重新调用组件函数、产出新的
虚拟 DOM14。注意这句话的精确含义,本书主线例子就建立在它上面:
「重新渲染」= React 把组件函数再调一遍。 它不意味着真实 DOM 被重写—— 真实 DOM 改不改、改哪里,由第 ③ 步的比对结果说了算15。
书里还补了 2026 年的坐标:React 早已不只是「虚拟 DOM 库」——18 版加入 并发渲染(React 可以暂停、恢复手头工作以保持界面响应)和 Suspense (以声明式方式处理异步数据和代码加载);更新的版本又加入 Server Components 和 Server Actions,把客户端渲染和服务器渲染的界线也模糊了16。 这本书的主体仍然是经典的客户端 SPA,但这些名词后面会再次出现。
4. 主线例子:一个计数按钮走完 React 的一生
Vite 脚手架生成的默认 App.tsx 里有一个计数按钮,这就是本章主线例子。
从头走到尾:
function App() {
const [count, setCount] = useState(0) // state:count,初值 0
return (
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
)
}第 ① 步:初始渲染。 main.tsx 是入口:createRoot 找到 HTML 里
id="root" 的那个空元素,把 <App /> 挂进去,整个应用从这棵树开始长17。
按钮第一次出现在屏幕上,文字是 count is 0。
第 ② 步:点击。 onClick 回调执行 setCount(0 + 1),state 从 0 变 1。
第 ③ 步:重新渲染。 state 变了,React 把 App 函数整个再调一遍。
这次 return 出来的描述里文字是 count is 1——这是内存里的新虚拟树15。
第 ④ 步:比对与提交。 React 拿新树和旧树比:整个按钮只有那行文字变了。 于是只更新文本节点这一个地方,按钮本身、页面其他部分一律不动13。
第 ⑤ 步:循环。 再点一次,0 → 1 → 2 → 3,每轮都是「调函数 → 比树 → 改最少的地方」。你写代码时从头到尾没有碰过一个 DOM API——这就是 声明式的实际体验11。
例子里还有两个 JSX 细节值得记下:JSX 不是 HTML,是 React 自创的语法,
最终被翻译成普通 JavaScript18;HTML 属性和 JavaScript 关键字撞名时
React 改名——class 是关键字,所以写 className19。
花括号 {} 在 JSX 属性里表示「这里面是代码」,不是字符串20。
5. 一个 React 项目由什么构成(Vite 实测)
npm create vite@latest 一条命令生成项目骨架;npm run dev 起开发服务器
(带热重载:改文件页面即时刷新);npm run build 产出部署用的最终文件,
放进 dist/ 目录21。2026 年时点的技术底座:Vite 8 已改用 Rust 写的
Rolldown 作为打包器,TypeScript 转译那层用 SWC(也是 Rust 写的)22。
生成的项目里值得认识的东西23:
| 文件/目录 | 干什么用 |
|---|---|
dist/ | 打包压缩后的最终产物(minification、tree shaking、code splitting 都在这步) |
node_modules/ | 从 npm 仓库下载的全部依赖 |
src/ | 全部源码;.tsx = React 组件,.ts = 纯 TypeScript,.css = 样式 |
package.json | 声明依赖与脚本;dependencies 是运行时依赖,devDependencies 只开发用 |
package-lock.json | 记录每个依赖的精确版本,保证别处装出来一模一样 |
@types/ 包 | 给非 TypeScript 写的包补类型信息,让编译器查得动 |
tsconfig.app.json / tsconfig.node.json | 两份配置:前管浏览器里跑的代码,后管 Vite 自己跑在 Node 里的配置 |
组件的最小形状长这样——FC 是 Functional Component(函数组件)的缩写;
用 Hooks 的话,函数组件是唯一的选择(旧的 class 组件写法已被淘汰)24:
const Home: FC = () => {
return <div>Hello World! Home</div>;
};
export default Home;一个组件要被看见,就得被另一个组件包进去——App 是根,所有组件最终都
挂在这棵树上25。return 多个同级元素时需要外壳,<></> 这个不留痕迹的
外壳叫 React Fragment(片段)26。
6. 作者的判断与证据
- 「SPA 的目标是原生应用的体验」是整个论证的起点,证据是对比经典模型 「任何内容变化都要整页回服务器」与原生模型「界面在本地代码里画」45。
- 「虚拟 DOM 不是性能银弹,声明式才是价值」是明确纠正流行的误解,并给了依据: 渲染目标被抽象,所以 React Native 才可能存在12。
- 并发渲染、Suspense、Server Components 的提及是事实性描述,非作者判断16。
7. 边界与局限
- SSR/Next.js 只有一句脚注的量:原书承认 Next.js、React Router(原 Remix) 「模糊了客户端渲染与服务器渲染的界线」27,但整本书的落点仍是纯客户端 SPA; 想看服务端渲染怎么落地,本书给不了。
useState在这里只是露脸:本章出现了钩子但没有解释原理,「第 5 章详讲」 是刻意留白。- Vite 的具体架构(Rolldown/SWC)是 2026 年时点,Vite 换代快,数字会过期, 「脚手架 → dev/build 两条命令」的用法不会。
判断(我们的,不是书里的): 把「重新渲染 ≠ 重写 DOM」在第 4 章就钉死, 是这本书前端部分最重要的早期投资——社区里大量「React 性能焦虑」 (动不动就 memo、动不动就怕重渲染)都源于把这两件事混为一谈。 先立正确心智模型再教 Hooks,顺序是对的。 如果错,会错在: 「重渲染廉价」的前提是组件树健康;一旦有人把昂贵的 计算直接塞进渲染体,第 ③ 步「每点一次全调一遍」就真的会变慢—— 所以第 5 章的
useMemo们不是装饰,是这个模型的必要补丁。
8. 可带走的
- 经典 Web 以「页」为单位:任何内容变化都整页重载;SPA 把应用整个下载到客户端换响应速度。
- SPA 四特征:单张 HTML、客户端渲染、预加载资源、虚拟路由(URL 只是内部门牌)。
- React 四想法:组件(分工)、声明式(风格)、虚拟 DOM + 渲染/提交两阶段(手段)、 state/props(扳机)。
- 虚拟 DOM 的价值是声明式模型和渲染目标抽象,不是比原生 DOM 快。
- 「重新渲染」= 组件函数被再调一遍;真实 DOM 改不改、改哪里,由比对决定。
- JSX 不是 HTML:
className代替class,花括号里是代码不是字符串。 - 用 Hooks 就只有函数组件一条路;
FC是它的类型名。 - 多元素要外壳时用
<></>(Fragment)。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| CGI 与服务器端渲染 | Chapter 4: Learning Single Page Application Concepts with React | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:63(搜「Server-Side Rendering」) |
| 原生应用对照组 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:81(搜「entire app is downloaded」) |
| SPA 四特征 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:125(搜「Single HTML Page」) · text/36-ch04-chapter-4-learning-single-page-application-conce.txt:137(搜「Virtual Routing」) |
| DOM 的定义 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:189(搜「Document Object Model」) |
| 声明式渲染 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:213(搜「imperatively」) |
| 虚拟 DOM 误解的纠正 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:251(搜「common misconception」) |
| 渲染阶段与提交阶段 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:239(搜「render phase」) |
| 重新渲染 ≠ 重写 DOM | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:275(搜「calls the component function again」) |
| 现代 React 的新能力 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:285(搜「concurrent rendering」) |
| createRoot 入口 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:688(搜「createRoot」) |
| JSX 不是 HTML / className | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:792(搜「not actually HTML」) · text/36-ch04-chapter-4-learning-single-page-application-conce.txt:810(搜「className」) |
| Vite 与 Rolldown | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:393(搜「Rolldown」) |
| FC 与函数组件唯一性 | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1052(搜「functional component」) |
| Fragment | 同上 | text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1101(搜「React Fragment」) |
Footnotes
-
出处:「Chapter 4: Learning Single Page Application Concepts with React」第 47–49 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:47,搜「static HTML pages」)。 ↩ -
出处:同上第 55–65 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:63,搜「Server-Side Rendering」)。 ↩ -
出处:同上第 67–77 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:77,搜「full page reload」)。 ↩ -
出处:同上第 79–87 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:81,搜「downloaded and installed」)。 ↩ ↩2 -
出处:同上第 109–117 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:113,搜「redraw portions」)。 ↩ ↩2 -
出处:同上第 123–155 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:125,搜「Single HTML Page」)。 ↩ -
出处:同上第 135–155 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:153,搜「postback is never triggered」)。 ↩ -
出处:同上第 119 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:119,搜「modern tooling」)。 ↩ -
出处:同上第 175–197 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:185,搜「logical tree」)。 ↩ -
出处:同上第 201–207 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:203,搜「Components」)。 ↩ -
出处:同上第 209–217 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:215,搜「telling the browser how to update the DO」)。 ↩ ↩2 -
出处:同上第 249–259 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:251,搜「common misconception」)。 ↩ ↩2 -
出处:同上第 219–247 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:233,搜「not against the browser DOM」;:239,搜「render phase」)。 ↩ ↩2 -
出处:同上第 261–275 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:267,搜「re-renders」)。 ↩ -
出处:同上第 275 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:275,搜「calls the component function again」)。 ↩ ↩2 -
出处:同上第 277–303 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:285,搜「concurrent rendering」)。 ↩ ↩2 -
出处:同上第 683–724 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:688,搜「createRoot」;:720,搜「renders our React Components」)。 ↩ -
出处:同上第 792–798 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:794,搜「custom JavaScript syntax」)。 ↩ -
出处:同上第 800–816 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:810,搜「className」)。 ↩ -
出处:同上第 958–980 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:964,搜「not strings」)。 ↩ -
出处:同上第 429–463 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:443,搜「hot reloading」;:453,搜「production build」)。 ↩ -
出处:同上第 391–397 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:393,搜「Rolldown」)。 ↩ -
出处:同上第 465–607 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:471,搜「dist」;:531,搜「most important folder」)。 ↩ -
出处:同上第 1026–1054 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1052,搜「functional component」;:1054,搜「only way」)。 ↩ -
出处:同上第 926–940 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:940,搜「root parent component」)。 ↩ -
出处:同上第 1097–1111 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1101,搜「React Fragment」)。 ↩ -
出处:同上第 129–133 段(
text/36-ch04-chapter-4-learning-single-page-application-conce.txt:131,搜「blur this line」)。 ↩