写了五章原生 HTML/CSS/JS 之后学框架,你会突然理解框架到底解决了什么问题:第 5 章「改数据要手动改 DOM」的机械劳动(改完数据改视图,两处状态同步靠自觉),框架把它变成了「只改数据,视图自动跟」。本章以 React 和 Vue 两大主流为例,讲清组件化、两条响应式路线、虚拟 DOM 的得失,最后给一个「什么时候不用框架」的反直觉建议。
组件 = 模板 + 逻辑 + 样式的封包,页面是组件树:
// React:JSX,标记写在 JS 里(HTML 进了表达式世界)
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
<!-- Vue:单文件组件 SFC,模板/逻辑/样式三段式 -->
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
两家的组件通信规则一致:props 向下传(父到子),事件向上抛(子到父),数据单向流动。这个「单向数据流」让「谁改了状态」可追溯,比 jQuery 时代「任何代码都可以改任何 DOM」的世界可维护得多。第 5 章的 TodoList(手写 DOM 操作 + 事件委托)用框架重写一遍,「数据驱动 vs DOM 驱动」的差别会瞬间体感化:框架版代码里几乎没有 document. 字样。
框架的核心魔法「数据变,视图自动变」,两家实现路线完全不同:
Vue:依赖收集(细粒度)。ref/reactive 把数据包成可观测对象,组件渲染时「读了哪些数据」被自动记录(依赖收集),之后改数据,只通知用到它的地方:
state.count++; // 直接改,响应式系统自己知道谁该更新
React:不可变 + 整树重渲染 + diff。状态被设计为不可变(必须 setCount 换新值,直接改旧值框架感知不到),状态变了就重新执行整个组件函数,生成新的虚拟 DOM 树,diff 新旧两棵树,把最小差异提交给真 DOM:
setCount(c => c + 1); // 不能 count++,React 靠新旧对比感知变化
两条路线的气味差异:Vue 「直接改数据」贴近直觉,但依赖追踪有魔法感;React 「改了会全函数重跑」反直觉,但行为可预测(纯函数:相同输入相同输出),Hooks 的心智模型(useState、useEffect、依赖数组)全建立在这套不可变哲学上。面试常问「Vue 和 React 的响应式差异」,答案就是这两段。
虚拟 DOM = 用 JS 对象描述 DOM 结构,状态变化时先 diff 新旧对象树,一次性提交最小变更到真 DOM:
状态变化 → 生成新虚拟树 → diff 旧树 → 最小 patch 提交真 DOM
功:批量提交(第 6 章回流优化的自动化,不再手写 DocumentFragment)、跨平台(虚拟树可以渲染到原生移动端/Canvas)、把「手动管 DOM」的易错全藏起来。过:diff 本身有成本(整树对比在 JS 里跑),React 组件函数重渲染的开销在大列表上肉眼可见(所以有 memo、useMemo、列表 key)。当代的反思也值得知道:Solid/Svelte 把「找出变化」挪到编译期(生成精确更新的代码),运行时虚拟树都不建了,性能更高。「虚拟 DOM 是必须的吗」已经从信仰问题变成工程权衡问题。
跨组件共享状态(登录用户、购物车),props 层层透传(props drilling)会疯掉,于是有集中式 store:React 生态的 Zustand/Redux(单向数据流:action → reducer → 新状态),Vue 生态的 Pinia(直接利用响应式)。入门建议:先硬用 props 透传撑到难受,再引入 store,你能体会它解决的是真实痛点而不是仪式。
反直觉建议压轴:内容型网站(博客、文档、企业官网)先考虑不用框架。判断标准三个问:页面状态多不多(交互复杂度)、数据要不要频繁驱动视图、SEO/首屏重不重要。内容型站点用「服务端渲染 + 少量原生 JS」(这个 wiki 就是:Next.js 但内容基本静态)或者纯静态 + 少量脚本,加载快、维护简单;仪表盘、编辑器这类「应用」才是框架的主场。框架是工具不是信仰,第 6 章的浏览器原理 + 第 5 章的原生 DOM 能力,才是「用不用框架」的裁判。
user.name = x 不触发更新(不可变是 React 感知变化的唯一途径),必须 setState 新对象。const { count } = reactive(obj) 拆出来的 count 是普通值;用 toRefs 或整体引用。state.push(...)(直接改数组),观察视图不更新;换成 [...state, item],验证不可变的必要性。