跳转至

单页应用与 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. 可带走的

  1. 经典 Web 以「页」为单位:任何内容变化都整页重载;SPA 把应用整个下载到客户端换响应速度。
  2. SPA 四特征:单张 HTML、客户端渲染、预加载资源、虚拟路由(URL 只是内部门牌)。
  3. React 四想法:组件(分工)、声明式(风格)、虚拟 DOM + 渲染/提交两阶段(手段)、 state/props(扳机)。
  4. 虚拟 DOM 的价值是声明式模型和渲染目标抽象,不是比原生 DOM 快。
  5. 「重新渲染」= 组件函数被再调一遍;真实 DOM 改不改、改哪里,由比对决定。
  6. JSX 不是 HTML:className 代替 class,花括号里是代码不是字符串。
  7. 用 Hooks 就只有函数组件一条路;FC 是它的类型名。
  8. 多元素要外壳时用 <></>(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」)


  1. 出处:「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」)。 ↩

  2. 出处:同上第 55–65 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:63,搜「Server-Side Rendering」)。 ↩

  3. 出处:同上第 67–77 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:77,搜「full page reload」)。 ↩

  4. 出处:同上第 79–87 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:81,搜「downloaded and installed」)。 ↩↩

  5. 出处:同上第 109–117 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:113,搜「redraw portions」)。 ↩↩

  6. 出处:同上第 123–155 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:125,搜「Single HTML Page」)。 ↩

  7. 出处:同上第 135–155 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:153,搜「postback is never triggered」)。 ↩

  8. 出处:同上第 119 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:119,搜「modern tooling」)。 ↩

  9. 出处:同上第 175–197 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:185,搜「logical tree」)。 ↩

  10. 出处:同上第 201–207 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:203,搜「Components」)。 ↩

  11. 出处:同上第 209–217 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:215,搜「telling the browser how to update the DO」)。 ↩↩

  12. 出处:同上第 249–259 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:251,搜「common misconception」)。 ↩↩

  13. 出处:同上第 219–247 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:233,搜「not against the browser DOM」;:239,搜「render phase」)。 ↩↩

  14. 出处:同上第 261–275 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:267,搜「re-renders」)。 ↩

  15. 出处:同上第 275 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:275,搜「calls the component function again」)。 ↩↩

  16. 出处:同上第 277–303 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:285,搜「concurrent rendering」)。 ↩↩

  17. 出处:同上第 683–724 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:688,搜「createRoot」;:720,搜「renders our React Components」)。 ↩

  18. 出处:同上第 792–798 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:794,搜「custom JavaScript syntax」)。 ↩

  19. 出处:同上第 800–816 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:810,搜「className」)。 ↩

  20. 出处:同上第 958–980 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:964,搜「not strings」)。 ↩

  21. 出处:同上第 429–463 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:443,搜「hot reloading」;:453,搜「production build」)。 ↩

  22. 出处:同上第 391–397 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:393,搜「Rolldown」)。 ↩

  23. 出处:同上第 465–607 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:471,搜「dist」;:531,搜「most important folder」)。 ↩

  24. 出处:同上第 1026–1054 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1052,搜「functional component」;:1054,搜「only way」)。 ↩

  25. 出处:同上第 926–940 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:940,搜「root parent component」)。 ↩

  26. 出处:同上第 1097–1111 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:1101,搜「React Fragment」)。 ↩

  27. 出处:同上第 129–133 段(text/36-ch04-chapter-4-learning-single-page-application-conce.txt:131,搜「blur this line」)。 ↩