背了忘、忘了背?不用了。这篇文章不罗列 API,只讲一件事------数据变了,页面怎么跟上。看完你就能串起 Vue 和 React 各自的整条链路。
开篇:为什么需要理解原理?
前端框架迭代快,新特性层出不穷,但翻来覆去就一条主线:数据变了,页面怎么跟上。 每个框架都在用不同的路径解决同一个问题。
Vue 的做法是:编译阶段分析模板 → 响应式系统追踪数据变化 → 精准更新 DOM。
React 的做法是:组件函数重新执行 → 新旧虚拟 DOM 对比 → 批量更新 DOM。
两条路线各有取舍,理解了取舍,你就不再纠结哪个更好------而是知道什么时候该用什么。
第一章:Vue 3
1.1 版本现状
截至 2026 年 6 月,Vue 3 稳定版为 3.5.35 。从 3.0 到 3.5,ref、reactive、computed、watch 这套 API 完全互通,不存在 Vue 2 升 Vue 3 那种大规模重写。
正在开发中的 Vue 3.6带来了两个重要变化:
- Vapor Mode:把模板直接编译成 DOM 操作代码,跳过虚拟 DOM 的创建和 diff。
- alien-signals:底层依赖追踪从 Map/Set 换成双向链表。数据变化时只做一个轻量的"标记为脏"(push),真正读数据时才重新计算值(pull),减少了不必要的重复计算。
尤雨溪在 2025 年 State of Vue.js 讨论中明确过:即便未来有 Vue 4,也只会极少量的破坏性变更,不会重演 Vue 2 到 3 的大规模迁移。团队策略是把大特性消化在 3.x 里。
1.2 核心原理:一条流水线,三步走
Vue 的运转可以概括为:编译 → 响应式 → 渲染。我们从一个最简单的计数器看起:
vue
<script setup>
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
第一步:编译------模板变代码
.vue 文件里的 <template> 浏览器不认识,Vue 的编译器把它翻译成渲染函数(JavaScript 函数),执行这个函数就会生成一棵虚拟 DOM 树。
编译器做了两个关键优化:
静态提升------模板里永远不变的部分,只创建一次,之后复用:
js
const _hoisted = createVNode("span", null, "静态标题") // 编译时一次搞定
render() {
return createVNode("div", null, [
_hoisted, // 每次 render 直接复用
createVNode("span", null, dynamicText, 1 /* TEXT */)
])
}
Patch Flags------编译器分析出每个动态节点"哪部分会变",给节点打一个数字标记:
| 标记 | 含义 | 例子 |
|---|---|---|
1 (TEXT) |
只有文本内容会变 | {{ message }} |
2 (CLASS) |
只有 class 会变 | :class="{ active }" |
4 (STYLE) |
只有 style 会变 | :style="{ color }" |
8 (PROPS) |
只有属性值会变 | :id="dynamicId" |
运行时 diff 看到标记是 1(TEXT),就知道只需比对文本,不用碰 class、style、属性。编译阶段分析得越清楚,运行时干的活就越少。这是 Vue 性能的核心秘密。
第二步:响应式------数据哪里变了,它就通知谁
Vue 3 用 ES6 的 Proxy 做响应式。你大概理解成这样就行:
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // "有人读我了,记下来"
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // "值变了,通知所有读过我的人重跑"
return true
},
})
}
但 const count = ref(0) 有个细节:0 是原始值(数字),不是对象。Proxy 只能代理对象,不能代理一个数字。所以 Vue 把 0 装进一个 { value: 0 } 的小盒子,再对这个盒子套一层 Proxy。读 count.value 就是打开盒子------触发 get,通知 Vue"有人在用这个数据";写 count.value = 1 就是往盒子里放新东西------触发 set,通知所有依赖者重跑。
如果用 reactive({ count: 0 }),那直接 state.count 就行,不需要 .value,因为 reactive 接收的本来就是对象。
为什么推荐 ref 而不是 reactive? reactive 有一个陷阱:解构就丢响应式。
js
const state = reactive({ name: "Lee" })
const { name } = state // name 现在是字符串 'Lee',跟 state 的 Proxy 断开关系了
改 name 不会触发更新。ref 不存在这个问题,因为 .value 始终指向那个盒子。
Vue 3.5+ 正式引入了 Reactive Props Destructure 特性。编译器会自动处理
defineProps的解构逻辑,确保解构后的值依然是响应式的。
shallowRef 干嘛的? 有时候数据很大(比如上万条数据的列表),你不希望 Vue 递归地把每个字段都变成 Proxy------太慢了。shallowRef 只追踪 .value 的替换,不追踪内部属性的变化。适合存大对象。
和 React 的根本区别 :Vue 知道"这个组件读了 count,那个组件没读",所以 count 变了只重跑读过的组件。React 默认父组件跑,所有子组件都跟着跑,需要开发者手动用 React.memo 阻断。
第三步:渲染------算出差异,更新最小范围
数据变了 → 渲染函数重新执行 → 产出新的虚拟 DOM 树 → diff 算法对比新旧两棵树。
普通 diff 的写法是:拿到两棵树,挨个节点对比 tag 类型、属性、文本、子节点......每个节点走一遍全流程。但 Vue 不这么干,因为编译阶段已经把"谁会变"标好了。
拿一个带静态内容的模板举例:
html
<div>
<h1>商品详情</h1> <!-- 永远不变 -->
<p>{{ price }}</p> <!-- 只有文本会变 -->
<span :class="statusClass"> <!-- 只有 class 会变 -->
{{ statusText }}
</span>
</div>
编译器处理后的效果(简化版):
js
const _hoisted_h1 = createVNode("h1", null, "商品详情")
// ↑ 静态节点只创建一次,后面永远复用
render() {
return createVNode("div", null, [
_hoisted_h1, // 直接用,跳过 diff
createVNode("p", null, price, 1 /* TEXT */), // 只比对文本
createVNode("span", { class: statusClass }, statusText, 2 | 1 /* CLASS + TEXT */)
// ↑ 编译器精确标出:class 和文本可能变,别的属性不用管
])
}
diff 时就按标记走捷径:
- 碰到
_hoisted_h1------静态的,跳过整个节点。 - 碰到
p标记为1(TEXT)------只比对新旧文本,class/style/属性一律不看。 - 碰到
span标了2 | 1------只看 class 和文本,跳过 style、属性等其他维度。
这样一来,一个几十个节点的组件树,diff 实际碰的可能就三四个节点,每处也只比对该比的那一两项。开销从"遍历整棵树"变成"精准点射几个点"。这就是编译期分析实打实省出来的性能。
第二章:React
2.1 版本现状
React 最新稳定版是 19.2.7(2026 年 6 月 2 日发布),同日还发了 19.1.8 和 19.0.7 补丁,覆盖三条版本线。
演进时间线:
| 版本 | 时间 | 核心变化 | 解决了什么 |
|---|---|---|---|
| React 16 | 2017.09 | Fiber 架构 | 渲染可以打断,不再卡界面 |
| React 18 | 2022.03 | 并发模式、自动批处理 | 更新有优先级,用户操作永远先响应 |
| React 19 | 2024.12 | Server Components、Server Actions | 组件可以在服务端跑,直接调数据库 |
React 19 是个分水岭。它新增了 Server Components ------组件只跑在服务端,不打包 JS,能直接读数据库。还有 Server Actions ------前端 <form> 直接调用服务端函数。
2.2 核心原理:从一口气跑完到随时刹车
2.2.1 React 15 的问题:停不下来
React 15 的 diff 算法是递归的。状态一变,从根组件开始往下遍历整棵树,一口气算完所有差异,中间没法停。树大了,JS 线程占满,浏览器卡死------用户点按钮没反应,动画卡帧。
2.2.2 Fiber:造一个能暂停的"栈"
React 16 的解法很巧妙:不用 JS 原生的函数调用栈(不可中断),换成链表。每个组件变成一个 Fiber 节点,三个指针连在一起:
child→ 第一个子节点sibling→ 下一个兄弟节点return→ 父节点
React 用 while 循环遍历这个链表,每处理完一个节点就检查:时间片用完了吗?有更紧急的事吗?有就停,没有就继续。
整个工作分成两步:
- Render 阶段(可暂停):在内存里算出所有差异,不碰真实 DOM
- Commit 阶段(不能停):把算好的差异一次性写到 DOM
时间切片粒度约 5ms:处理一组工作,用完 5ms 就把控制权还给浏览器。
2.2.3 React 18:给更新排优先级
Fiber 让 React 能刹车,React 18 让它知道什么时候该刹车:
- 并发模式:不要把所有更新一视同仁。用户敲键盘是紧急的,搜索框下面的列表过滤可以慢一点。紧急的更新可以"插队",不紧急的先让路。
startTransition:手动标记"这个更新不紧急"。比如搜索时,输入文字的更新定为高优,过滤结果的更新定为低优------打字不卡,列表可以晚半拍。
jsx
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e) {
setQuery(e.target.value) // 高优:立刻更新输入框
startTransition(() => {
setResults(filterData(e.target.value)) // 低优:列表可以慢慢来
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>过滤中...</span>}
<ResultList data={results} />
</>
)
}
setQuery 直接调,敲字不卡;setResults 包在 startTransition 里,React 会在空闲时再算,如果有新的打字进来,上一次还没算完的过滤就直接丢弃。
Suspense:声明式的"还没加载好"状态。<Suspense fallback={<Loading />}><SlowComponent /></Suspense>------SlowComponent 没准备好就自动显示 Loading。- 自动批处理 :同一事件回调里多次
setState,React 自动合成一次渲染。React 17 只能在onClick这类事件里批处理,setTimeout或 fetch 回调里三次setState就是三次渲染------老代码得用unstable_batchedUpdates手动合并。React 18 全场景自动处理了。
2.2.4 React Compiler:自动帮你阻断无效渲染
Vue 能精确知道谁变了,React 做不到------它默认父组件跑,所有子组件跟着跑。所以 React 开发者在代码里大量塞 useMemo/useCallback/React.memo 来手动阻断。
React Compiler 在编译阶段自动帮你干这件事。2025 年 10 月已发布 v1.0,目前处于 RC 阶段,React 官方建议所有项目使用。Next.js 16 已将 reactCompiler 从 experimental 提升为稳定配置项。不过它并非万能:代码必须遵守 React 规则,不守规则的部分编译器会自动跳过,效果仍因项目而异。
第三章:横向对比
核心差异一句话
| 维度 | Vue 3 | React 19 |
|---|---|---|
| 响应式 | 自动追踪,变了谁通知谁 | 默认父组件更新则子组件全部重新执行,手动或编译器阻断 |
| 渲染 | 编译期分析模板,精准定位变化 | 运行时时间切片,保证不长时间卡主线程 |
| 组件 | .vue 单文件 |
JSX/TSX 函数 |
| 状态管理 | Pinia(官方) | Zustand / Jotai(社区) |
| 路由 | Vue Router(官方) | React Router / TanStack Router |
| SSR | Nuxt 3 | Next.js |
怎么选
- 团队小、跑得快、不想纠结选型 → Vue。全家桶一路通。
- 项目大、交互复杂、需要最丰富的生态 → React。选型自由,并发渲染原生支持。
- 做全栈、重 SEO → 都行。Nuxt 3 和 Next.js 都很成熟,Next.js 的 Server Components 是个独特优势。
- 做移动端 → React Native 生态更成熟。
趋势
两条路在趋同。Vapor Mode 抛弃虚拟 DOM 走编译路线,React Compiler 在编译期注入优化------都在把运行时的工作搬到编译期。ECMAScript Signals 提案已经 Stage 1,两个框架的响应式思路在标准层面也在靠拢。
版本的更替不会停,但"编译 → 响应式 → 渲染"这条主线八九不离十。
总结一下:
- Vue 在编译阶段就把模板分析透了,响应式系统做到精准追踪,运行时只动该动的。适合不想纠结太多、快速出活的团队。
- React 用 Fiber 和并发模式在运行时安排一切,灵活度高、生态最广。需要开发者自己做好性能取舍,但 React Compiler 正在把这件事自动化。
- 两者正在往同一个方向走------编译器承担更多,运行时越来越轻。
没有最好的框架,只有最适合你当前项目的选择。
如果这篇文章帮你串起了 Vue 和 React 的底层逻辑,欢迎点赞、收藏,方便以后回看。你目前在用哪个框架?觉得它的设计哲学对日常工作影响大不大?评论区聊聊。
参考资料