【前端架构和框架】编程范式和框架

78 阅读8分钟

前面文章介绍了前端范式迁移,这篇文章我们先来看下编程范式变化,上篇文章我们简单看下命令式和声明式的区别,这里面我们详细看下编程阶段的不同细节。然后比较下前端比较热门框架和库在实现编程范式上的权衡和考量。无论采取哪一种方式实现,效率和质量都是要考虑的最核心的因素,效率包括开发习惯,快速搭建、复用性、可维护性、可读性;质量包括用户体验、性能、健壮性等。

一、编程范式(命令式和声明式)

命令式

关注过程。开发者需要一步步告诉浏览器 “做什么”(比如先获取 DOM、再修改样式、再更新文本、最后处理事件),每一步操作都需要手动控制。jQuery 的 UI 更新机制:直接操作DOM,状态管理与 DOM 操作紧密耦合:

  1. 状态存储方式

    • 状态通常存储在 DOM 元素的 data 属性中,或全局变量中
    • 没有统一的状态管理机制
  2. UI 更新流程

    用户交互 → 事件处理器 → 直接修改DOM → 状态与DOM同步 → UI
    
  3. 工作原理

    • 开发者手动编写 DOM 操作代码来反映状态变化
    • 状态变化和 UI 更新是同一操作的两个部分
    • 没有抽象层,直接操作浏览器 DOM API
  4. 示例

// 手动选择元素 → 绑定事件 → 更新数据 → 操作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);
});

声明式

关注结果。开发者只需要描述 “想要什么结果”,不需要手动编写 “如何实现这个结果” 的步骤,由框架去做。

  1. 状态存储方式

    • 状态存储在组件内部(useState 或 this.state)
    • 状态是不可变的,必须通过特定方法更新
  2. UI 更新流程

    用户交互 → 事件处理器 → 更新状态 → UI (React重新渲染 → DOM差异更新)
    
  3. 工作原理

    • 开发者只需要关注状态更新,UI 渲染由 React 自动处理
    • React 通过虚拟 DOM 计算前后差异,只更新必要的 DOM 节点
    • 状态是唯一数据源,保证 UI 与状态一致
  4. 示例

// 只描述状态与视图的映射关系
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 之间的中间层

  • 工作方式

    1. 状态变化时,先构建新的虚拟 DOM 树
    2. 通过 Diff 算法对比新旧虚拟 DOM 的差异
    3. 只将变化的部分更新到真实 DOM
  • 架构特点

    • 所有逻辑主要在运行时处理
    • 虚拟 DOM 提供了跨平台能力(可渲染到 DOM、原生组件等)
    • 灵活性高,但存在一定的运行时开销
Vue:运行时 + 编译时的混合架构
  • 核心机制:响应式系统 + 虚拟 DOM 的结合

  • 工作方式

    1. 通过 Proxy(Vue 3)或 Object.defineProperty(Vue 2)追踪状态依赖
    2. 状态变化时,仅通知依赖该状态的组件
    3. 组件内部通过虚拟 DOM 计算差异并更新
  • 架构特点

    • 模板在编译时会被优化(如静态节点提升)
    • 响应式系统实现了细粒度的更新通知
    • 兼顾灵活性和性能,平衡了开发体验
Svelte:编译时主导的无运行时架构
  • 核心机制:编译器直接将组件转换为原生 DOM 操作代码

  • 工作方式

    1. 构建时分析组件代码,确定状态与 DOM 的依赖关系
    2. 生成针对性的 DOM 更新代码(无需虚拟 DOM)
    3. 状态变化时直接执行优化后的原生 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(ifmap 等)
  • 需遵循 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、性能与体积对比

维度ReactVueSvelte
运行时性能中(虚拟 DOM 开销)高(响应式 + 编译优化)极高(直接 DOM 操作)
初始加载性能中(框架体积较大)高(体积适中 + 按需引入)极高(几乎无框架体积)
更新性能中(需 Diff 整个组件树, 死磕运行时)高(细粒度依赖追踪+diff,编译时+运行时,组件级)极高(编译时优化的更新逻辑,没有diff,元素级)

三、总结

三大框架的核心差异源于对 "抽象层次" 和 "性能 / 灵活性权衡" 的不同选择:

  • React 选择了最高的抽象层次(虚拟 DOM),牺牲了一些性能换取最大的灵活性和跨平台能力
  • Vue 选择了平衡的抽象层次(响应式系统 + 虚拟 DOM),在开发体验、性能和灵活性之间取得平衡
  • Svelte 选择了最低的抽象层次(编译时直接生成原生代码),牺牲了一些灵活性换取最佳性能和最小体积

框架的底层设计和实现没有好坏之分,只是不同点(开发体验、灵活性、加载性能、渲染性能、可维护性等)的考量罢了。学习过程中只有理解这些核心差异,才能在项目中做出合理选择,并充分发挥框架的优势。