Skip to content

前端框架的本质

💡 学习指南:这篇文章会回答一个根本问题——前端框架(Vue、React、Svelte 等)到底在做什么? 如果你只学过 HTML、CSS 和一点 JavaScript,完全没问题,我们从头讲起。

在开始之前,先确认你知道这两个基础概念。如果不确定,可以先看对应章节:

  • HTML:网页的骨架,定义页面上有哪些元素(标题、段落、按钮、图片……)。参见 HTML 与 CSS 布局
  • JavaScript:让网页"动起来"的编程语言,可以修改页面内容、响应用户操作。参见 JavaScript 深度指南

还有一个概念会在后面频繁出现,这里先做一个完整的说明。

什么是 DOM?

DOM 的全称是 Document Object Model,中文叫"文档对象模型"。

当你在浏览器中打开一个网页时,浏览器做的第一件事就是读取 HTML 代码。读完之后,浏览器不会直接拿 HTML 文本去显示页面,而是先把 HTML 代码转换成一棵树形结构,存放在内存里。这棵树就叫 DOM 树。

树上的每一个节点(Node)对应 HTML 里的一个标签。标签之间的嵌套关系,在 DOM 树里就变成了父节点和子节点的关系。

👇 动手试试看: 把鼠标移到左边的 HTML 代码上,右边 DOM 树中对应的节点会高亮。反过来也一样。每一行 HTML 标签都对应 DOM 树上的一个节点。

HTML → DOM 树浏览器如何理解你写的 HTML
你写的 HTML 代码
1<html>
2<body>
3<h1>我的购物车</h1>
4<p>共 3 件商品</p>
5<ul>
6<li>耳机</li>
7<li>键盘</li>
8<li>鼠标</li>
9</ul>
10<button>结算</button>
11</body>
12</html>
浏览器解析
浏览器生成的 DOM 树
html
└─body
└─h1"我的购物车"
└─p"共 3 件商品"
└─ul
└─li"耳机"
└─li"键盘"
└─li"鼠标"
└─button"结算"
📄
节点(Node)DOM 树上的每一个方块就是一个节点。每个 HTML 标签(如 <h1><p>)都对应一个节点。
🌳
父子关系标签嵌套在另一个标签里面,在 DOM 树上就是父节点和子节点的关系。<body> 里包含 <h1>,所以 body 是 h1 的父节点。
✏️
DOM 操作JavaScript 可以增加、删除、修改 DOM 树上的节点。修改节点后,浏览器会重新计算布局并重新绘制页面,这就是"DOM 操作"。
关键概念:DOM 是浏览器在内存中维护的一棵树,它和你写的 HTML 一一对应。JavaScript 无法直接修改 HTML 文件,它修改的是这棵 DOM 树——浏览器再根据 DOM 树的变化更新屏幕上的显示。

为什么要了解 DOM? 因为 JavaScript 修改页面的方式,就是操作这棵 DOM 树——增加节点、删除节点、修改节点的内容。而前端框架做的核心工作,就是帮你自动化这些 DOM 操作。后面我们会反复提到 DOM,理解它是理解框架原理的基础。


0. 引言:什么是"前端框架"?

先解释"框架"这个词。在编程中,框架(Framework) 是一套已经写好的代码和规则,它规定了你的代码应该怎么组织、怎么运行。你按照它的方式写代码,它帮你处理大量重复、繁琐的底层工作。

前端框架,就是专门帮你构建网页界面的框架。目前最常见的有 Vue、React、Svelte、Angular 这几个。

那它们到底帮你解决了什么问题?下面这三张卡片概括了核心逻辑:

问题
  • 数据变化时,手动更新 DOM 容易遗漏
  • 页面越复杂,需要同步的地方越多,越容易出 bug
  • 多人协作时,DOM 操作散落各处,维护成本高
根本原因
  • 浏览器不知道"数据"和"界面"的对应关系
  • 原生 DOM API 只提供底层操作,没有"数据变了就更新 UI"的能力
  • 开发者被迫充当"人肉同步器"
框架的解法
  • 建立数据到 UI 的映射关系(UI = f(State))
  • 自动检测数据变化(响应式系统)
  • 自动计算最小 DOM 更新(虚拟 DOM / 编译优化)

接下来我们一步步展开,从最基础的问题讲起。


1. 核心问题:数据变了,界面怎么办?

1.1 先搞清楚"数据"和"界面"是什么

在任何一个网页应用中,都有两个东西在同时存在:

  • 数据(Data / State):程序内部存储的信息。比如"购物车里有 3 件商品"、"用户名是张三"、"当前选中了第 2 个标签页"。这些数据存在 JavaScript 的变量里,用户看不到它们。
  • 界面(UI):用户在屏幕上看到的东西。比如页面上显示"购物车(3)"、显示"欢迎,张三"、第 2 个标签页高亮。这些是 HTML 元素呈现出来的视觉效果。

数据和界面之间有对应关系:数据是"3 件商品",界面上就应该显示"3"。如果数据变成了"4 件商品",界面上也应该跟着变成"4"。

问题是:这个"跟着变"的过程,谁来负责?

👇 动手点点看: 点击"添加商品"按钮,注意观察:数据(左边)已经变了,但界面(右边)没有跟着更新——它们之间"断开"了。再点"同步界面"手动修复。

数据(JavaScript 变量)
商品数量0
总价¥0
状态正常
✅ 同步
界面(用户看到的)
购物车0 件
总价¥0
状态正常
核心问题:在没有框架的情况下,数据变了,界面不会自动跟着变。你必须自己写代码去更新界面,一旦忘了,用户看到的就是过时的、错误的信息。

1.2 为什么 JavaScript 变量变了,界面不会自动变?

这是零基础最容易困惑的地方,我们把底层原理一步步讲清楚。

在 JavaScript 中,变量就是一块内存空间,用来存放数据。当你执行 count = count + 1 时,JavaScript 引擎做的事情非常简单:把内存中 count 这个位置的值从 3 改成 4。做完这一步就结束了,不会再发生任何事。

而页面上显示的内容(比如 <span>3</span> 这个 DOM 节点)存放在另一块完全不同的内存空间里。JavaScript 引擎在修改变量时,根本不知道页面上有一个 DOM 节点正在显示这个变量的值,也没有任何机制让它去检查。

所以本质原因是:JavaScript 的变量和 DOM 节点是两块独立的内存,它们之间没有任何自动联动机制。 修改变量只改变了变量所在的内存,DOM 节点所在的内存不会受到任何影响。

javascript
let count = 3

// 页面上有一个 DOM 节点显示着 count 的值:
// <span id="counter">3</span>

count = 4
// JavaScript 引擎做了什么?
//   → 把变量 count 在内存中的值从 3 改成 4
//   → 结束。没了。
// 页面上 <span> 里显示的仍然是 "3"

如果你想让页面上的显示也变成"4",你必须额外写代码,手动找到那个 DOM 节点,然后修改它的内容:

javascript
count = 4  // 第 1 步:改变量

// 第 2 步:你必须自己写——找到 DOM 节点,把它的文字改成新值
document.getElementById('counter').textContent = count

如果页面上有 5 个地方显示着 count 的值(购物车数量、商品列表、总价、小计、状态提示),你就需要写 5 段这样的代码。漏掉任何一段,那个位置显示的就还是旧值,用户看到的就是错误信息。

1.3 框架做了什么?两步建立自动连接

框架能自动同步,靠的是两步配合——缺一不可。

第一步:你在模板里"登记"哪些地方要显示这个变量

框架的 HTML 模板里,你用 这样的语法来标记"这里要显示 count 的值":

html
<!-- Vue 模板 -->
<span>购物车:{{ count }} 件</span>    <!-- 位置 A:我要显示 count -->
<span>总价:¥{{ count * 99 }}</span>   <!-- 位置 B:我也用了 count -->
<span>{{ count > 5 ? '过多' : '正常' }}</span>  <!-- 位置 C:我也用了 count -->

框架第一次渲染页面时,会把这个"登记关系"记录下来:位置 A、B、C 都依赖 count

第二步:框架监视变量,变了就查登记表、自动更新

框架用 JavaScript 内置的 Proxy(代理)把你的变量"包裹"起来,让它变成一个"被监视的变量"。当你修改这个变量时,Proxy 会在赋值的同时悄悄多做一件事:通知框架"count 变了"。框架收到通知后,去查第一步的登记表,把 A、B、C 三个位置全部更新。

原生 JS:
  你写 HTML → <span id="counter">3</span>(和变量无任何连接)
  你改变量 → count = 4 → 结束,界面毫无反应
  你手动补 → document.getElementById('counter').textContent = 4 → 界面才更新

Vue 框架:
  你写模板 → <span>{{ count }}</span>(框架记住:这里依赖 count)
  你改变量 → count = 4 → Proxy 拦截 → 通知框架 → 框架查登记表 → 自动更新 A/B/C

这就是为什么"只有框架才能自动同步"——原生 HTML 里的 <span> 和 JS 变量之间根本没有任何连接,框架的模板语法()才是建立这条连接的关键。你写了 ,框架才知道这里要显示 count;框架才能在 count 变化时,精准找到这里并更新它。

👇 动手点点看: 先选"原生 JavaScript",点"执行"后注意观察——变量改了但界面纹丝不动,你要一步步手动同步每个位置。再切换到"使用框架",同样点"执行"——变量一改,框架自动完成所有步骤,界面立刻跟上。

变量修改时发生了什么?原生 JavaScript vs 框架
你写的代码
// 点击按钮时执行
count = count + 1
// 你还要手动写下面这些:
document.getElementById('count')
.textContent = count
document.getElementById('total')
.textContent = count * 99
执行流程
1
JavaScript 修改变量
count 从 0 变成 1
2
找到 DOM 节点
手动调用 document.getElementById()
3
修改 DOM 内容
手动调用 .textContent = 新值
界面结果
购物车0 件
总价¥0
为什么不自动?JavaScript 的变量是"无感知"的。你执行 count = 4 时,JavaScript 引擎只是把内存中 count 的值从 3 改成 4,仅此而已。它不会通知任何人,不会触发任何回调,不会去检查页面上哪里显示了 count。所以界面不会有任何变化——除非你自己写代码去更新 DOM。

1.4 对比:手动同步 vs 自动同步的实际效果

理解了原理之后,我们来看看在一个稍微复杂一点的场景下,手动同步和自动同步的区别有多大。

👇 动手点点看: 左边是没有框架时的"手动同步"方式——每个显示区域你都需要单独点"同步"按钮来更新。右边是有框架时的"自动同步"方式——你只管点"添加商品",所有显示区域自动更新。试试在左边故意不同步某个区域,看看会发生什么。

手动同步 / jQuery 风格
🔴购物车数量已同步
0 件
📋商品列表已同步
(空)
💰总价已同步
¥0
⚠️状态提示已同步
正常
遗漏次数:0
VS
自动同步 / 框架风格
🔴购物车数量已同步
0 件
📋商品列表已同步
(空)
💰总价已同步
¥0
⚠️状态提示已同步
正常
遗漏次数:0
核心思想:前端框架的本质价值在于"自动同步"——你只需修改数据,框架保证所有依赖该数据的 UI 自动更新,不会遗漏。

这就是前端框架存在的根本原因:给 JavaScript 变量加上"被修改时自动通知界面更新"的能力,消灭手动同步带来的错误。


2. 框架的核心思想:用数据描述界面

2.1 两种写法的区别

理解了"自动同步"的价值之后,我们来看框架具体是怎么实现的。

在没有框架的时代(比如使用 jQuery),代码是这样写的——你一步一步告诉浏览器该做什么:

javascript
// 第 1 步:找到页面上 id 为 counter 的元素
var element = document.getElementById('counter')
// 第 2 步:把这个元素的文字内容改成新的值
element.textContent = '4'
// 第 3 步:找到另一个元素,也改掉
document.getElementById('total').textContent = '¥396'
// 第 4 步:如果数量大于 5,还要改状态提示……

这种写法叫命令式(Imperative)——你在"命令"浏览器一步步执行操作。

有了框架之后,代码变成这样——你只描述"界面应该长什么样":

html
<!-- 我不管这个值怎么更新到页面上的 -->
<!-- 我只说:这里应该显示 count 的值 -->
<span>{{ count }}</span>
<span>总价:¥{{ count * 99 }}</span>
<span v-if="count > 5">商品过多!</span>

这种写法叫声明式(Declarative)——你在"声明"界面的最终状态,至于怎么达到这个状态,框架自己处理。

2.2 核心公式:UI = f(State)

所有现代前端框架——不管是 Vue、React 还是 Svelte——都遵循同一个核心思想,可以用一个公式来表达:

UI = f(State)

这个公式的意思是:

  • State(状态):你的应用数据。就是 JavaScript 里的那些变量:购物车里有几件商品、用户有没有登录、当前页面是哪个……
  • f(函数):框架的渲染机制。它知道怎么把数据变成界面。
  • UI(界面):用户在屏幕上看到的最终结果。

含义:给定一组数据(State),经过框架的处理(f),就能确定性地得到对应的界面(UI)。数据变了,界面就跟着变。开发者只需要关心数据,不需要关心界面怎么更新。

👇 动手点点看: 在左边修改数据(State),观察右边的界面(UI)如何自动跟着变化。这就是 UI = f(State) 的直观体现。

State(数据)
→ f →
UI(界面)
修改数据(State)
2
渲染结果(UI)
你好,访客!
购物车:2 件商品
总价:¥198
当前主题:浅色
当前 State 快照
{ "username": "(空)", "count": 2, "darkMode": false }
核心思想:你只需要修改数据(State),框架会根据数据自动渲染出对应的界面(UI)。同样的数据永远渲染出同样的界面,这就是 UI = f(State)。

2.3 为什么声明式比命令式好?

声明式写法的优势在于:

对比维度命令式(没有框架)声明式(有框架)
代码量每个更新都要写具体操作代码只写一次模板,框架自动处理
出错概率容易漏更新某个地方框架保证所有地方都更新
可读性代码里混杂着大量 DOM 操作代码清晰地描述界面结构
维护成本修改一个功能要改很多地方修改数据逻辑即可,界面自动跟随

简单说:声明式让你把精力集中在"业务逻辑"(数据怎么变化)上,不用操心"界面怎么更新"这个重复且容易出错的事情。


3. 响应式系统:框架如何知道数据变了?

3.1 什么是"响应式"?

前面说了"数据变了,界面自动更新"。但这里有一个技术问题:JavaScript 本身并没有"变量被修改时自动通知别人"的能力

你写 count = 4,JavaScript 只是把 count 的值从 3 改成 4,不会自动告诉任何人。框架需要一种机制来"发现"你修改了数据。

响应式(Reactivity) 就是这种机制的总称:当数据发生变化时,系统能自动感知到变化,并执行相应的更新操作。

3.2 三种不同的实现方式

不同的框架采用了不同的技术方案来实现响应式。这也是 Vue、React、Svelte 之间最根本的区别。

方式一:代理拦截(Vue 的做法)

Vue 使用 JavaScript 内置的 Proxy(代理)机制。Proxy 可以在你读取或修改一个对象的属性时,自动执行一段你指定的代码。

Vue 把你的数据对象用 Proxy 包裹起来。当你执行 count = 4 时,Proxy 会拦截这次写入操作,通知 Vue:"count 的值变了",然后 Vue 去更新所有用到 count 的界面部分。

你作为开发者不需要做任何额外的事情——直接赋值就行,Vue 自动感知。

方式二:显式调用(React 的做法)

React 不使用 Proxy。它要求你必须通过一个专门的函数来修改数据:

javascript
// React 的写法
const [count, setCount] = useState(0)

// 不能直接写 count = 4(React 不会感知到)
// 必须调用 setCount:
setCount(4)

只有当你调用 setCount() 时,React 才知道数据变了,才会去更新界面。如果你直接写 count = 4,React 完全不知道,界面不会更新。

这种方式更"显式"——每一次数据变化都是你主动告诉框架的,不会有意外的更新。

方式三:编译器分析(Svelte 的做法)

Svelte 采用了完全不同的路线。它有一个编译器(Compiler),在你的代码运行之前,编译器会先分析你的源代码。

当编译器看到你写了 count += 1 这样的赋值语句时,它会自动在这行代码后面插入一段"通知界面更新"的代码。也就是说,在代码运行的时候,"通知"这个动作已经被编译器提前安排好了。

你的代码看起来就是普通的 JavaScript 赋值,但编译后的代码里多了更新界面的逻辑。

👇 动手点点看: 选择不同的框架标签,点击"修改数据",观察每种框架在"引擎盖下"经历了哪些步骤来完成数据变化的检测和界面更新。

count:0
引擎盖下
1count = 1 → Proxy 的 set 陷阱被触发
2通知依赖收集器:"count 变了"
3找到所有依赖 count 的组件
4自动更新 DOM
核心思想: Vue 通过 Proxy 自动拦截数据读写,开发者无需额外操作——写法最自然。

3.3 三种方式的对比

对比维度Vue(Proxy 代理)React(显式调用)Svelte(编译器)
开发者写法直接赋值 count = 4必须用 setCount(4)直接赋值 count = 4
感知变化的时机运行时自动拦截开发者主动通知编译时提前插入通知代码
运行时性能开销Proxy 有少量拦截开销setState 调度有少量开销几乎没有额外开销
调试难度中等数据流清晰,较容易需要理解编译后的代码
适合场景追求开发效率和自然写法追求可预测的数据流追求极致运行性能

三种方式没有绝对的好坏。Vue 写起来最自然,React 的数据流最可控,Svelte 的运行性能最好。选择哪个取决于项目的具体需求。


4. 组件:把界面拆成可复用的小块

4.1 为什么要拆?

一个完整的网页可能有导航栏、侧边栏、内容区、搜索框、用户头像、各种按钮……如果所有代码写在一个文件里,这个文件会变得非常长、非常难维护。

组件(Component) 就是把界面拆分成一个个独立的小块,每个小块管自己的数据、自己的界面、自己的逻辑。

比如一个电商页面可以拆成这些组件:

  • NavBar 组件:负责顶部导航栏
  • SearchBox 组件:负责搜索框
  • ProductCard 组件:负责一张商品卡片
  • ShoppingCart 组件:负责购物车

每个组件都是独立的。ProductCard 不需要知道 NavBar 里写了什么代码,它只需要管好自己。

4.2 组件的三个好处

好处一:复用。 一个 ProductCard 组件写好之后,可以在页面上用 100 次——每次传入不同的商品数据,就会渲染出不同的商品卡片。不需要复制粘贴 100 份 HTML 代码。

好处二:封装。 组件内部的数据和逻辑是独立的。修改 SearchBox 组件的代码,不会影响到 ProductCard 组件。多人协作时,不同的人可以同时开发不同的组件,互不干扰。

好处三:可维护。 当某个功能出了问题,你可以直接定位到对应的组件去修复,不需要在一个几千行的大文件里翻找。

👇 动手点点看: 点击左边的组件名称,查看它在页面上对应的区域。注意观察:同一个 ProductCard 组件被复用了多次,每次显示不同的数据。

组件化拆分一个页面如何拆成多个独立组件
组件树结构
📱App(根组件)
🧭NavBar(导航栏)
🔍SearchBox(搜索框)
🛒CartIcon(购物车图标)
📦ProductCard(商品卡片)×3
📄Footer(页脚)
页面预览
🏠 电商网站🔍 搜索框🛒 购物车(3)
📦
商品 1
¥199
📦
商品 2
¥298
📦
商品 3
¥397
📦 ProductCard(商品卡片)
单个商品的展示卡片。写一次代码,传入不同的商品数据就能复用多次,每次显示不同的商品信息。
数据独立样式隔离 复用 3 次
核心思想:组件化就是把一个大页面拆成多个独立的小块。每个组件管理自己的数据、界面和样式,互不干扰。同一个组件可以在不同地方复用多次,传入不同的数据就会显示不同的内容。

4.3 组件在代码里长什么样?

以 Vue 为例,一个组件就是一个 .vue 文件,里面包含三部分:

html
<!-- ProductCard.vue -->
<template>
  <!-- 这里写 HTML 结构 —— 组件的"外观" -->
  <div class="card">
    <h3>{{ name }}</h3>
    <p>价格:¥{{ price }}</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>

<script setup>
// 这里写 JavaScript 逻辑 —— 组件的"行为"
const props = defineProps(['name', 'price'])

function addToCart() {
  // 处理"加入购物车"的逻辑
}
</script>

<style scoped>
/* 这里写 CSS 样式 —— 组件的"样式" */
.card {
  border: 1px solid #ccc;
  padding: 16px;
}
</style>

使用这个组件时,就像使用一个自定义的 HTML 标签:

html
<!-- 在其他地方使用 ProductCard 组件 -->
<ProductCard name="无线耳机" price="299" />
<ProductCard name="机械键盘" price="599" />
<ProductCard name="显示器" price="1999" />

三行代码就渲染出了三张不同的商品卡片。


5. DOM 操作的代价:为什么框架要费这么大力气?

5.1 什么是 DOM 操作?

前面提到过 DOM——浏览器把 HTML 解析后生成的树形结构。DOM 操作就是用 JavaScript 去修改这棵树上的节点。比如改一段文字、增加一个元素、删除一个元素、修改一个样式。

这些操作本身不复杂,但是浏览器在执行 DOM 操作之后,需要做很多额外的工作才能让屏幕上的显示更新:

  1. 重新计算样式:这个节点以及它的子节点的 CSS 样式是否需要变化?
  2. 重新布局(Layout / Reflow):页面上所有元素的位置和大小需要重新计算。因为一个元素的改变可能影响到其他元素的位置。
  3. 重新绘制(Paint):把计算好的内容画到屏幕上。

这三个步骤每一个都有计算成本。如果你的代码频繁触发 DOM 操作,浏览器就会反复执行这些步骤,页面就会变卡。

👇 动手点点看: 观察直接操作 DOM 和批量操作 DOM 的耗时对比。当修改次数增多时,"逐个操作"的耗时会急剧上升。

DOM 操作耗时对比逐个操作 vs 批量操作
逐个操作 DOM
每修改一次数据 → 立刻操作一次真实 DOM → 浏览器每次都要重新布局和绘制
模拟耗时
1修改 → 布局 → 绘制
2修改 → 布局 → 绘制
3修改 → 布局 → 绘制
4修改 → 布局 → 绘制
... 重复 16 次 ...
批量计算后一次性操作
所有修改先在内存中计算好 → 最后只操作一次真实 DOM → 浏览器只需要重新布局和绘制一次
模拟耗时
1内存中计算 20 次变化
2一次性提交 → 布局 → 绘制
核心思想:DOM 操作的真正代价不是"修改值"本身,而是每次修改后浏览器必须执行的"重新布局 + 重新绘制"。减少 DOM 操作次数,就是减少这些昂贵的计算。虚拟 DOM 的作用就是先在内存中算好所有变化,最后一次性提交。

5.2 框架怎么解决这个问题?

既然直接操作 DOM 很昂贵,框架就想办法减少 DOM 操作的次数。具体有两种策略:

策略一:虚拟 DOM + 差异比较(Vue、React 的做法)

虚拟 DOM(Virtual DOM)是一个 JavaScript 对象,它的结构和真实 DOM 树一一对应,但它只存在于内存中,不会触发浏览器的布局和绘制。

当数据变化时,框架的处理流程是:

  1. 用 JavaScript 对象创建一棵"新的虚拟 DOM 树",描述数据变化后界面应该长什么样
  2. 把这棵新树和旧树做对比(这个过程叫 Diff,即差异比较),找出哪些节点发生了变化
  3. 只把真正变化的部分应用到真实 DOM 上(这个过程叫 Patch,即打补丁)

这样一来,不管数据怎么变化,最终对真实 DOM 的操作总是最少的。

👇 动手点点看: 点击"修改数据",观察虚拟 DOM 如何对比新旧两棵树,找出变化的节点。注意看最右边的"真实 DOM"——只有真正变化的部分才会闪烁。

虚拟 DOM Diff 过程最小化 DOM 更新的核心机制
Old VTree
div.app
h1: 待办清单
ul.list
li: 学习 Vue
li: 写作业
li: 打游戏
Diff Result
div.app
h1: 待办清单
ul.list
li: 学习 Vue
li: 写作业
li: 打游戏
Real DOM
div.app
h1: 待办清单
  • 学习 Vue
  • 写作业
  • 打游戏
7
虚拟 DOM 节点总数
0
需要更新的真实 DOM
节省的 DOM 操作
核心思想: 虚拟 DOM 先在内存中对比新旧两棵树,找出最小差异,然后只更新必要的真实 DOM 节点——避免了大量无效操作。

策略二:编译时精确定位(Svelte 的做法)

Svelte 不使用虚拟 DOM。它的编译器在你写代码时就分析好了:"当 count 变化时,需要更新第 3 行的 <span> 元素"。运行时直接定位到那个元素去更新,完全不需要对比新旧树。

这种做法跳过了 Diff 步骤,理论上性能更好。但它依赖编译器的分析能力——编译器需要足够聪明才能正确识别出所有需要更新的地方。


6. 运行时 vs 编译时:框架设计的核心权衡

6.1 两个阶段

前端代码从你写下到最终在浏览器里运行,会经过两个阶段:

  • 编译时(Compile-time / Build-time):你的源代码被构建工具(如 Vite、Webpack)处理,转换成浏览器能直接执行的代码。这个过程发生在你的电脑上,在用户打开网页之前。
  • 运行时(Runtime):转换后的代码在用户的浏览器中执行。框架的核心逻辑(比如虚拟 DOM 的 Diff、响应式的追踪)就在这个阶段工作。

6.2 框架在这两个阶段的工作分配

不同框架在这两个阶段分配的工作量不同,这决定了它们的性能特征和包体积:

  • React:大部分工作在运行时完成。虚拟 DOM 的创建、Diff、Patch 都发生在浏览器中。好处是灵活性高;代价是需要把整个框架的运行时代码(约 40KB)发送给浏览器。
  • Vue:混合方式。模板在编译时被优化(编译器标记出哪些节点是静态的、不会变化的),但最终的界面更新仍然通过运行时的虚拟 DOM 完成。运行时代码约 30KB。
  • Svelte:大部分工作在编译时完成。编译器分析你的代码,直接生成精确的 DOM 更新指令。运行时几乎没有框架代码——最终打包出来只有你自己的业务代码。包体积最小。

👇 动手点点看: 点击不同的框架标签,查看它们在"运行时 ↔ 编译时"光谱上的位置,以及各自在打包体积、运行性能、开发体验上的权衡。

框架光谱运行时 ↔ 编译时
更多运行时更多编译时
ReactVue 3Vue VaporSvelteSolid.js
💚Vue 3
混合:编译优化模板 + 运行时虚拟 DOM
运行时工作量
60%
编译时工作量
40%
打包体积中等开发体验★★★★★
趋势: 趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。

6.3 行业趋势

近几年框架的发展方向很明确:把越来越多的工作从运行时移到编译时。因为编译时的计算不占用用户的设备资源,不影响页面加载速度。

  • Vue 正在开发 Vapor Mode(蒸汽模式),可以跳过虚拟 DOM,在编译时直接生成 DOM 操作代码
  • React 推出了 React Compiler,在编译时自动优化组件的重渲染行为
  • Svelte 5 引入了 Runes 系统,进一步增强编译时的分析能力

7. 总结

回顾这篇文章的核心要点:

前端框架解决的根本问题:当应用中的数据发生变化时,自动、高效、可靠地更新界面,不需要开发者手动操作 DOM。

它们共同遵循的核心思想:UI = f(State)——界面是数据的函数,开发者只需关注数据的变化,框架负责把数据的变化反映到界面上。

它们的关键技术差异

技术点含义
响应式系统框架如何检测数据变化。Vue 用 Proxy 拦截、React 用显式 setState、Svelte 用编译器分析。
虚拟 DOMVue 和 React 用一个 JavaScript 对象来模拟 DOM 树,通过对比新旧两棵树(Diff)来找出最小更新量,减少真实 DOM 操作。
组件化把界面拆成独立的、可复用的小块,每个组件管理自己的数据和界面。
编译时优化在代码构建阶段提前做分析和优化,减少运行时的计算量。Svelte 在这方面走得最远。

一句话:前端框架的本质工作就是——接管"数据到界面"的同步过程,让开发者只需要思考数据逻辑,不再需要手动操作界面。


名词对照表

英文术语中文对照解释
Framework框架一套预先编写好的代码和规则,为开发者提供应用的基础结构和常用功能。
DOM文档对象模型浏览器把 HTML 解析后生成的树形数据结构,JavaScript 通过操作它来修改页面。
Virtual DOM虚拟 DOM用 JavaScript 对象模拟 DOM 树,通过 Diff 算法找出最小更新路径,减少真实 DOM 操作次数。
State状态应用中的数据,比如用户信息、购物车内容、页面当前状态等。
Reactivity响应式当数据变化时,系统能自动感知并执行对应的界面更新操作。
Proxy代理JavaScript 内置机制,可以拦截对一个对象的读取和写入操作。Vue 3 用它来实现响应式。
Component组件一段独立的、可复用的界面代码,包含自己的 HTML 结构、JavaScript 逻辑和 CSS 样式。
Declarative声明式一种编程方式:你描述"最终想要什么结果",由框架来决定怎么实现。
Imperative命令式一种编程方式:你一步一步告诉程序"具体怎么做"。
Diff差异比较对比新旧两棵虚拟 DOM 树,找出哪些节点发生了变化。
Patch打补丁把 Diff 找到的变化部分,应用到真实 DOM 上。
Compile-time编译时代码在构建阶段被处理的时期,发生在用户打开网页之前。
Runtime运行时代码在用户浏览器中执行的时期。
Compiler编译器一个程序,把源代码转换成另一种形式的代码。Svelte 的编译器把 .svelte 文件转换成高效的 JavaScript。