前面文章介绍了前端范式迁移,这篇文章我们先来看下编程范式变化,上篇文章我们简单看下命令式和声明式的区别,这里面我们详细看下编程阶段的不同细节。然后比较下前端比较热门框架和库在实现编程范式上的权衡和考量。无论采取哪一种方式实现,效率和质量都是要考虑的最核心的因素,效率包括开发习惯,快速搭建、复用性、可维护性、可读性;质量包括用户体验、性能、健壮性等。
一、编程范式(命令式和声明式)
命令式
关注过程。开发者需要一步步告诉浏览器 “做什么”(比如先获取 DOM、再修改样式、再更新文本、最后处理事件),每一步操作都需要手动控制。jQuery 的 UI 更新机制:直接操作DOM,状态管理与 DOM 操作紧密耦合:
-
状态存储方式:
- 状态通常存储在 DOM 元素的 data 属性中,或全局变量中
- 没有统一的状态管理机制
-
UI 更新流程:
用户交互 → 事件处理器 → 直接修改DOM → 状态与DOM同步 → UI -
工作原理:
- 开发者手动编写 DOM 操作代码来反映状态变化
- 状态变化和 UI 更新是同一操作的两个部分
- 没有抽象层,直接操作浏览器 DOM API
-
示例:
// 手动选择元素 → 绑定事件 → 更新数据 → 操作DOM
// 1. 状态存储在DOM上
$('.counter').data('count', 0);
// 2. 用户交互触发事件
$('.increment-btn').click(function() {
// 3. 读取当前状态
const currentCount = $('.counter').data('count');
// 4. 计算新状态
const newCount = currentCount + 1;
// 5. 更新状态存储
$('.counter').data('count', newCount);
// 6. 手动更新UI
$('.counter-display').text(newCount);
});
声明式
关注结果。开发者只需要描述 “想要什么结果”,不需要手动编写 “如何实现这个结果” 的步骤,由框架去做。
-
状态存储方式:
- 状态存储在组件内部(useState 或 this.state)
- 状态是不可变的,必须通过特定方法更新
-
UI 更新流程:
用户交互 → 事件处理器 → 更新状态 → UI (React重新渲染 → DOM差异更新) -
工作原理:
- 开发者只需要关注状态更新,UI 渲染由 React 自动处理
- React 通过虚拟 DOM 计算前后差异,只更新必要的 DOM 节点
- 状态是唯一数据源,保证 UI 与状态一致
-
示例:
// 只描述状态与视图的映射关系
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 2. 事件处理器仅关注状态更新
const handleName = () => {
// 3. 更新状态(不可变更新)
setName('zhangshan')
};
return (
<div>
<span className={count > 0 ? 'highlight' : ''}>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
<div onClick={()=>handleName()}}>{name}<div>
</div>
);
}
二者核心区别:声明式本质是屏蔽DOM操作细节,基于状态/数据驱动视图,状态和视图绑定映射关系,通过事件改变状态/数据,自动更新视图快照。
// 业务逻辑去修改数据,然后扔给框架去做更新,不关心框架里怎么做的
UI= F(state)
另外从UI更新流程上看,声明式用户操作的链路更短
命令式: 用户交互 → 事件处理器 → 直接修改DOM → 状态与DOM同步 → UI
声明式: 用户交互 → 事件处理器 → 更新状态 → UI (从状态到UI更新,由框架内部做)
注:声明式框架内部其实还是命令式实现的,只是对外提供了声明式的方式给开发者使用。
二、各框架的 “声明式” 考量的点
React、Vue 和 Svelte 作为当前主流的声明式前端框架,虽然都遵循 "描述 UI 应该是什么样子" 的设计理念,但在实现机制、核心架构和使用场景上存在根本性差异。这些差异源于它们对 "如何高效连接状态与 UI"。
1、核心架构差异:运行时 vs 编译时
三大框架最根本的区别在于处理状态更新与 UI 渲染的时机—— 是在运行时动态处理,还是在编译时提前优化。
React:运行时主导的虚拟 DOM 架构
-
核心机制:采用虚拟 DOM(Virtual DOM)作为状态与真实 DOM 之间的中间层
-
工作方式:
- 状态变化时,先构建新的虚拟 DOM 树
- 通过 Diff 算法对比新旧虚拟 DOM 的差异
- 只将变化的部分更新到真实 DOM
-
架构特点:
- 所有逻辑主要在运行时处理
- 虚拟 DOM 提供了跨平台能力(可渲染到 DOM、原生组件等)
- 灵活性高,但存在一定的运行时开销
Vue:运行时 + 编译时的混合架构
-
核心机制:响应式系统 + 虚拟 DOM 的结合
-
工作方式:
- 通过 Proxy(Vue 3)或 Object.defineProperty(Vue 2)追踪状态依赖
- 状态变化时,仅通知依赖该状态的组件
- 组件内部通过虚拟 DOM 计算差异并更新
-
架构特点:
- 模板在编译时会被优化(如静态节点提升)
- 响应式系统实现了细粒度的更新通知
- 兼顾灵活性和性能,平衡了开发体验
Svelte:编译时主导的无运行时架构
-
核心机制:编译器直接将组件转换为原生 DOM 操作代码
-
工作方式:
- 构建时分析组件代码,确定状态与 DOM 的依赖关系
- 生成针对性的 DOM 更新代码(无需虚拟 DOM)
- 状态变化时直接执行优化后的原生 DOM 操作
-
架构特点:
- 几乎没有运行时依赖(框架代码被编译到业务代码中)
- 编译时完成大部分优化工作
- 最终代码体积小,运行性能接近原生
2、状态管理机制差异
状态管理是连接数据与 UI 的核心环节,三大框架采用了截然不同的实现方式。
React:应用级更新(显式更新与单向数据流)
-
状态存储:组件内部通过
useState或useReducer管理 -
更新触发:必须通过
setState或状态更新函数显式触发 -
数据流:严格的单向数据流,父组件通过 props 向下传递数据
-
特点:
- 状态更新是不可变的(推荐创建新对象而非修改原对象)
- 默认进行组件级更新,可通过
React.memo等 API 优化 - 复杂状态管理需依赖外部库(如 Redux、Zustand)
Vue:(组件级更新)响应式自动追踪
-
状态存储:组件的
data函数或ref/reactive创建的响应式变量 -
更新触发:状态被修改时自动触发更新(发布订阅+依赖收集)
-
数据流:默认单向,但支持
v-model语法糖实现双向绑定 -
特点:
- 响应式系统自动追踪依赖,无需手动调用更新函数
- 细粒度更新(只更新使用该状态的 DOM 节点)
- 内置
Vuex/Pinia处理全局状态
Svelte:(元素级更新)编译时绑定与赋值更新
-
状态存储:组件内直接声明的变量(无需特殊 API)
-
更新触发:对变量赋值时自动触发更新(编译时注入更新逻辑)
-
数据流:支持单向和双向绑定
-
特点:
- 无需虚拟 DOM 和响应式系统,通过编译时分析实现更新
- 状态更新语法与原生 JavaScript 一致(直接赋值)
- 内置简单的状态管理能力,复杂场景可使用
svelte/store
3、声明式UI表示层
框架的语法直接影响开发体验和学习曲线。
React:JSX 语法(HTML in JavaScript)
jsx
function TodoList({ todos, onToggle }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
))}
</ul>
);
}
- HTML 直接写在 JavaScript 中,完全融合
- 使用 JavaScript 表达式(
{ })嵌入动态内容 - 条件渲染和循环依赖原生 JavaScript(
if、map等) - 需遵循 JSX 特定规则(如
className而非class)
Vue:模板语法(HTML 扩展)
vue
<template>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:style="{ textDecoration: todo.done ? 'line-through' : 'none' }"
@click="onToggle(todo.id)"
>
{{ todo.text }}
</li>
</ul>
</template>
<script setup>
defineProps(['todos', 'onToggle']);
</script>
- 默认使用 HTML 模板,通过指令扩展功能
- 使用
{{ }}输出动态内容,v-for处理循环 - 事件绑定(
@click)和属性绑定(:style)有专门语法 - 逻辑与模板分离,更符合传统前端开发习惯
Svelte:HTML-like 模板(逻辑与标记融合)
svelte
<ul class="todo-list">
{#each todos as todo (todo.id)}
<li
style="text-decoration: {todo.done ? 'line-through' : 'none'}"
on:click={() => onToggle(todo.id)}
>
{todo.text}
</li>
{/each}
</ul>
<script>
export let todos;
export let onToggle;
</script>
- 模板中可直接使用
{#if}、{#each}等控制流语法 - 事件绑定使用
on:click语法,属性直接使用原生 HTML 语法 - 变量和逻辑直接在模板中访问,无需特殊标记
- 风格更接近原生 HTML,但有自定义语法规则
4、性能与体积对比
| 维度 | React | Vue | Svelte |
|---|---|---|---|
| 运行时性能 | 中(虚拟 DOM 开销) | 高(响应式 + 编译优化) | 极高(直接 DOM 操作) |
| 初始加载性能 | 中(框架体积较大) | 高(体积适中 + 按需引入) | 极高(几乎无框架体积) |
| 更新性能 | 中(需 Diff 整个组件树, 死磕运行时) | 高(细粒度依赖追踪+diff,编译时+运行时,组件级) | 极高(编译时优化的更新逻辑,没有diff,元素级) |
三、总结
三大框架的核心差异源于对 "抽象层次" 和 "性能 / 灵活性权衡" 的不同选择:
- React 选择了最高的抽象层次(虚拟 DOM),牺牲了一些性能换取最大的灵活性和跨平台能力
- Vue 选择了平衡的抽象层次(响应式系统 + 虚拟 DOM),在开发体验、性能和灵活性之间取得平衡
- Svelte 选择了最低的抽象层次(编译时直接生成原生代码),牺牲了一些灵活性换取最佳性能和最小体积
框架的底层设计和实现没有好坏之分,只是不同点(开发体验、灵活性、加载性能、渲染性能、可维护性等)的考量罢了。学习过程中只有理解这些核心差异,才能在项目中做出合理选择,并充分发挥框架的优势。