Skip to content
React 与 Vue 运行时模型、组件一等公民性及调度控制权
概述
React 和 Vue 的灵活性差异并非源于 JSX 与模板这类表层语法选择。三个架构层的根本分歧决定了各自的边界:运行时模型如何感知数据依赖、组件是否被视为语言层面的一等公民、以及更新调度的控制权归属。
运行时模型
React:基于重新执行的渲染
React 组件在状态变更时的行为是一次完整的函数重新执行。框架不维护数据与 UI 之间的依赖图。setState 调用触发对应组件的函数体再次运行,产出新的 VNode 树,再通过 diff 确定实际 DOM 操作。
jsx
function Counter() {
const [count, setCount] = useState(0)
// setCount 触发 Counter 重新执行。
// React 不记录 count 影响了哪些 DOM,仅重新执行函数体。
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}几个关键特征:
- 无自动依赖追踪。
useState不记录读取位置。useEffect的依赖数组由开发者手动声明,框架不会推导。 - 重新执行即重新闭包。每次渲染新建闭包,捕获当次的 props 与 state,形成快照式语义——UI 是某一时刻状态的反映。
- 框架职责仅限 diff 与 commit。Render 阶段生成 VNode 树(纯计算,可中断),Commit 阶段执行 DOM 变更(不可中断)。
由于缺少自动依赖追踪,渲染粒度与优化决策由开发者承担:
jsx
// 手动跳过子组件渲染
const MemoChild = React.memo(function Child({ data }) {
return <div>{data}</div>
})
// 手动缓存计算结果
const expensiveResult = useMemo(() => compute(a, b), [a, b])
// 手动保持函数引用稳定,避免子组件无效刷新
const handleClick = useCallback(() => { /* ... */ }, [dep])这些 API 的存在说明框架默认不做浅比较、不做计算缓存。开发者根据实际情况决定是否引入优化。
Vue:基于依赖追踪的响应式
Vue 3 使用 Proxy 在运行时建立完整的依赖图。ref() 或 reactive() 创建的数据在读取时触发 track(),在赋值时触发 trigger(),框架据此精确知道哪些组件需要更新。
vue
<script setup>
const count = ref(0)
// 模板中读取 count.value → 触发 track,建立依赖:count → 当前组件的 render effect
// count.value 修改时 → trigger → 仅重新运行当前组件的 render
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>Vue 编译器的静态分析进一步增强效率。SFC 的模板在编译阶段被转换为带 patchFlag 标记的 render 函数,运行时只对标记为动态的部分进行比对,静态节点被提升复用。
两个运行时模型的核心差异:
| 方面 | React | Vue |
|---|---|---|
| 依赖图维护方 | 开发者(依赖数组) | 框架(Proxy 自动收集) |
| 渲染触发方式 | 调用 setState 或 dispatch | 修改响应式数据的值 |
| 默认渲染粒度 | 组件子树(不优化时整个子树重新执行) | 组件级自动精确更新,编译器可细化到元素 |
| 跳过渲染的手段 | React.memo(手动) | 编译器根据 patchFlag 自动跳过静态部分 |
| 计算值缓存 | useMemo(手动声明依赖) | computed() 自动惰性求值 |
注意点
Vue 的依赖追踪减少了手动优化,但也引入了限制:不能在 setup() 外部任意修改响应式数据而不引起渲染;解构 ref 需要借助 toRefs 以保持响应性;ref 的 .value 包装在函数传递时需要注意。
React 的代价是默认过度渲染。React.memo、useMemo、useCallback 并非锦上添花的优化,而是对“默认全量重新计算”行为的必要修正。依赖数组手动维护存在遗漏风险。
组件的一等公民性
React:组件是函数
React 组件本质是返回 VNode 的普通函数。组件可以像任何 JavaScript 值一样被操作——赋值、传参、返回、条件调用。
jsx
// 组件作为 prop 传递
function Layout({ header: HeaderComp, children }) {
return (
<div>
<HeaderComp />
<main>{children}</main>
</div>
)
}
// 高阶组件:接收组件,返回新组件
function withAuth(Component) {
return function Authenticated(props) {
if (!useAuth()) return <Login />
return <Component {...props} />
}
}
// 条件选择不同组件——普通 JavaScript 表达式
const Link = variant === 'button' ? ButtonLink : TextLink
// children 可以是函数(render props)
function DataFetcher({ url, children }) {
const { data, loading } = useFetch(url)
return children({ data, loading })
}因为没有额外的框架抽象层,这些模式完全依赖语言能力。Headless UI 库(如 Radix UI、TanStack Table)得以通过 render props 或 children-as-function 将状态管理与渲染分离,无需引入新的 DSL。
Vue:组件是被编译系统理解的对象
Vue 的单文件组件(.vue)包含模板、脚本和样式三个部分,由编译器统一处理。最终产物是一个选项对象(defineComponent 返回),而非可以直接作为值传递的函数。
vue
<!-- UserCard.vue -->
<script setup>
defineProps(['user'])
</script>
<template>
<div>{{ user.name }}</div>
</template>
<style scoped>
div { padding: 4px; }
</style>.vue 文件不能被赋值给变量、作为参数随意传递,也不能在 .ts / .js 文件中像普通函数那样被条件调用。模板 DSL 中不支持直接使用组件表达式。Vue 通过作用域插槽提供了类似 render props 的能力,但这是模板语法的一部分,与语言原生表达能力仍有距离。使用 h() 函数可以编写 JSX/TSX,但会失去模板编译带来的静态提升和 patchFlag 优化。
这一差异导致 React 适合需要深度组件抽象的场景(如通过组件重组实现 A/B 测试、按条件返回不同组件),而 Vue 在高度结构化的模板编译中获得了更强的默认性能与更低的学习成本。
调度控制权
React:显式优先级控制
React 的 Fiber 架构配合 Lane 模型,允许开发者在触发更新时标记优先级。useTransition 将某些更新标记为低优先级,使其可被用户交互中断;useDeferredValue 延迟消费新值,保持旧值可见直到新值计算完成。
jsx
function SearchPage() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
function handleInput(e) {
// 高优先级:输入框文本立即更新
setQuery(e.target.value)
// 低优先级:搜索结果可在后台中断计算
startTransition(() => {
setSearchResults(expensiveSearch(data, e.target.value))
})
}
return (
<div>
<input value={query} onChange={handleInput} />
{isPending && <Spinner />}
<SearchResults results={searchResults} />
</div>
)
}并发模式下,render 阶段可以被浏览器中断(通过 shouldYield),在 5ms 内让出主线程处理用户输入,然后恢复未完成的渲染。
React 需要调度层来弥补运行时模型的不足:框架不了解数据依赖,默认会重渲染整个子树。提供可中断性和优先级控制,可以在不优化组件树的情况下避免长时间任务造成的掉帧。
Vue:内部批处理与自动调度
Vue 的调度逻辑内建在响应式系统中,对开发者透明。同步执行过程中触发的多个响应式变更会被微任务队列自动去重、排序,并在下一个 tick 统一执行。
js
// Vue 内部调度器概念简化
const queue = []
let isFlushing = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
if (!isFlushing) {
isFlushing = true
nextTick(flushJobs) // 微任务(Promise.resolve)
}
}
function flushJobs() {
queue.sort((a, b) => a.id - b.id) // 父组件先更新
for (const job of queue) {
job.run()
}
queue.length = 0
isFlushing = false
}Vue 的调度策略只负责批处理和排序,因为依赖追踪已经将变更精确到需要更新的组件。不需要像 React 那样通过可中断渲染来丢弃低优先级工作。开发者无法手动干预优先级,但这种自动行为在大多数表单、列表、动画场景中已经足够,且避免了错误使用 startTransition 导致的不一致问题。
React 灵活性的制约因素
手动优化负担
框架默认不缓存计算、不跳过子组件渲染,开发者需要在适当位置手工干预:
jsx
function UserList({ users, onSelect }) {
const [filter, setFilter] = useState('')
const filtered = users.filter(u => u.name.includes(filter))
// 每次渲染都会重新执行 filter。若 users 规模大,需要 useMemo 缓存。
const handleClick = (id) => onSelect(id)
// 每次渲染创建新函数引用,可能引发 memo 子组件不必要的刷新。
return filtered.map(u => (
<UserItem key={u.id} user={u} onClick={handleClick} />
// 若 UserItem 未包裹 React.memo,每次都会重新渲染。
))
}在 Vue 中,等价功能由 computed 自动缓存,编译器生成的 render 函数自动进行静态提升,模板中事件绑定也无需开发者维持函数引用稳定。
闭包与过时状态
“每次渲染都是全新闭包”的语义在定时器或异步回调中容易造成过时闭包:
jsx
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // 始终为 0(闭包捕获初始值)
setCount(count + 1) // 始终设为 1
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖表示 effect 只运行一次
}setInterval 回调捕获的是首次渲染的 count,后续渲染产生的新闭包对定时器无效。修正方式:使用函数式更新 setCount(c => c + 1) 避免依赖 count,或借助 useRef 作为可变容器暂存最新值。useRef 在 React 的不可变数据流中保留了可变的引用,与 Vue 的 ref 对象有相通之处——Vue 中 count.value 始终访问当前值,因为它是同一对象的属性读取,而非闭包变量。
Hook 调用顺序限制
React 使用链表存储 hook 状态,必须通过固定的调用顺序才能在重渲染时正确匹配。不能在条件、循环或嵌套函数中调用 hooks:
jsx
// 错误:hooks 调用顺序不稳定
if (condition) {
useState('x') // 若首次渲染条件为 true,二次渲染为 false → 顺序错乱
}Vue 的 Composition API 没有此限制,因为 setup() 函数仅执行一次,ref() 在那一次调用中创建并注册。
Vue 以约束换取的优化
Vue 的若干约束直接为编译优化和自动行为提供了前提。
- 模板语法限制:不能直接使用
map、filter,必须使用v-for;条件逻辑须用v-if/v-else。这些限制使编译器能精确标记动态内容(patchFlag),跳过大量不必要 diff。 - SFC 非纯 JavaScript:组件定义必须通过
.vue文件经编译管线处理。虽然不能像 React 那样在.ts中创建内联组件,但由此获得了scopedCSS、静态提升等编译优化。 - 响应式包装:
.value的显式解包规则与解构需toRefs辅助,这些额外步骤是依赖追踪的代价。React 中解构即用,但丢失了自动优化。
Vue 编译器对模板生成的 render 函数(概念简化):
js
const _hoisted_1 = createElementVNode("p", null, null, -1 /* HOISTED */)
function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.msg), 1 /* TEXT */)
// patchFlag: 1 表示只有文本是动态的,静态节点被提升
]))
}趋同方向
两个框架都在向中间靠拢。
React Compiler 能够静态分析组件内的依赖关系,自动注入等价于 useMemo 与 useCallback 的缓存逻辑,减少手动优化的需求。它的分析对象是 JavaScript,难度比分析模板更高,且作用域限于组件内部,不改变跨组件的运行时数据流。
Vue 的 Vapor Mode 去掉虚拟 DOM 中间层,将模板直接编译为操作 DOM 的指令,响应式系统直接与 DOM 绑定。这一步向细粒度更新模式演进。
两者的核心约束依然限制了趋同的上限:React 无法放弃 JSX 的灵活性而引入模板层面的严格静态分析;Vue 无法去掉 ref / reactive 的包装,因为那是依赖追踪的根基。
框架选择参考
选择 React 通常因为:
- 需要在不同渲染目标(React Native、React Three Fiber、Ink 等)上工作,React 的可插拔 reconciler 模型对此支持更好。
- 组件层面的抽象程度要求较高,例如 headless UI 模式、通过组件函数传递实现 A/B 切换或组合。
- 需要在特定场景下对渲染优先级做精细控制(
useTransition、Suspense)。 - 团队有 JavaScript 深度,愿意承担优化决策责任。
选择 Vue 通常因为:
- 中后台或表单密集型应用,模板指令与
v-model等语法可以减少代码量。 - 团队成员经验水平不一时,模板的约束编译能统一风格并减少性能审查负担。
- 期望较小的构建配置工作量(Vite + SFC 开箱即用,
<style scoped>省去额外 CSS 方案)。 - 需要渐进式引入:CDN 引入或嵌入现有系统,无需整体迁移技术栈。
React 的灵活性源于架构设计;Vue 的约束并非能力缺陷,而是以自动化换取特定场景效率的取舍。
参考链接
- React 官方文档:https://react.dev
- React Hooks 参考:https://react.dev/reference/react/hooks
- useTransition:https://react.dev/reference/react/useTransition
- React Compiler:https://react.dev/learn/react-compiler
- Vue 文档:https://cn.vuejs.org
- 深入响应式系统:https://cn.vuejs.org/guide/extras/reactivity-in-depth.html
- Vue Vapor Mode 设计讨论:https://github.com/vuejs/core-vapor
