Vanilla JS与Vue/React构建的网站源码有何区别
深入对比Vanilla JS与Vue/React在开发期源码与生产交付源码上的核心区别,剖析DOM更新机制、构建编译流程与实际选型考量。
在前端开发中,很多初学者在对比原生 JavaScript(Vanilla JS)与 Vue/React 时,往往把注意力放在学习曲线或开发速度上,但在实际工程中,最根本的区别集中在“源码形态”与“浏览器交付机制”上。简而言之:Vanilla JS 的源码遵循以 HTML 为宿主骨架、编写命令式指令直接操作浏览器真实 DOM 的模式,无需编译器即可原生运行;而 Vue/React 的源码则是高度组件化、声明式的逻辑集合,包含了浏览器无法直接识别的 JSX 或单文件组件(SFC),必须依赖构建工具(如 Vite、Webpack)将源代码转译、压缩、打包为最终的运行时 Bundle。理解这两种架构在源码上的差异,是读懂现代 Web 应用运行本质的关键前提。
一、源码形态的双重视角:开发期源码与浏览器接收源码
要客观比较源码的区别,必须区分两个完全不同的场景:一是开发者在编辑器中书写的开发期源码,二是用户通过浏览器“查看网页源代码”时实际接收到的生产交付代码。
1. 开发期源码的组织形态
在 Vanilla JS 项目中,代码结构通常高度扁平与直观:一个 index.html 负责结构,一个或多个 .css 文件负责样式,若干个 .js 文件负责交互。代码中没有抽象的语法糖,所有的脚本都使用标准的 ECMAScript 规范。项目可以完全没有 package.json,也不依赖安装外部工具链,直接双击 HTML 文件或通过任意静态服务器就能在浏览器中正常展示。
而在 Vue 或 React 项目中,开发期源码完全是组件驱动的工程化项目。React 使用以 .jsx 或 .tsx 结尾的文件,将 HTML 标签语法与 JavaScript 表达式融为一体;Vue 则使用 .vue 单文件组件,将 <template>、<script> 与 <style> 封装在一个文件中。项目中包含庞大的 node_modules、严格的编译配置(如 vite.config.js、tsconfig.json)以及模块导入路径别名。这类源码脱离了构建工具,浏览器直接加载会报错。
2. 浏览器接收到的交付产物
当我们在部署上线后的网站上右键点击“查看网页源代码”(View Page Source)时,两者的差异更为悬殊:
- Vanilla JS 站点:源码中包含了完整的页面骨架与文本内容,开发者在 HTML 里写了哪些标签,源代码里就呈现哪些标签,脚本文件通过普通的
<script src="app.js"></script>引入。 - Vue/React 客户端渲染(CSR)单页应用:网页源代码几乎是一片空白,通常只包含一个空的挂载节点(如
<div id="app"></div>或<div id="root"></div>),随后跟随若干个经过高度混淆、带内容哈希的静态资源包(如assets/index-D7a82c.js)。浏览器端展现出的丰富界面,全靠这几个 JS 脚本在运行时动态创建并插入 DOM。
二、编程范式与DOM操作差异:命令式调度 vs 声明式响应
在核心业务逻辑的编写上,源码展示出了截然不同的设计哲学:命令式编程(Imperative)与声明式编程(Declarative)。
1. 业务逻辑的代码实现对比
以实现一个常见的“计数器点击增加”功能为例,对比两者的原生代码形态:
Vanilla JS 的源码实现:
<!-- HTML 文件中先定义结构 -->
<div class="counter-box">
<p>当前计数:<span id="count-display">0</span></p>
<button id="inc-btn">点击增加</button>
</div>
<!-- JS 文件中编写操作指令 -->
<script>
let count = 0;
const countDisplay = document.getElementById('count-display');
const incBtn = document.getElementById('inc-btn');
incBtn.addEventListener('click', () => {
count += 1;
// 必须命令式调用 DOM 接口,手动完成文本更新
countDisplay.textContent = count;
});
</script>
Vue 3 的单文件组件源码实现:
<script setup>
import { ref } from 'vue';
// 仅声明数据状态
const count = ref(0);
const increment = () => {
count.value += 1; // 无需查找任何 DOM 节点,框架自动完成界面同步
};
</script>
<template>
<div class="counter-box">
<p>当前计数:<span>{{ count }}</span></p>
<button @click="increment">点击增加</button>
</div>
</template>
React 的函数组件源码实现:
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter-box">
<p>当前计数:<span>{count}</span></p>
<button onClick={() => setCount(prev => prev + 1)}>点击增加</button>
</div>
);
}
2. 代码背后的运行机制差异
从代码中可以看出,Vanilla JS 要求开发者充当“指令下达者”:你必须先精确找到元素对应的内存句柄(getElementById),然后手动监听事件,在状态改变时,再手动把新的值指派给元素的特定属性。如果页面逻辑复杂,状态分散在十几个变量中,开发者很容易遗漏某处 DOM 的同步,或者在多次频繁操作中引发不必要的浏览器回流(Reflow)与重绘(Repaint)。
而 Vue 和 React 的源码中根本没有直接的 document.querySelector 或 textContent 赋值。开发者只需要定义“数据是什么”以及“界面如何随数据变化”。框架底层的响应式依赖追踪(Vue 的 Proxy 监听)或虚拟 DOM 协调 Diff(React 的 Reconciler)会在状态变化时,自动计算出需要变更的最小 DOM 集合,并批量应用到页面上。
三、构建编译流水线:从人类可读源码到机器高效产物
Vanilla JS 几乎不存在构建阶段,编写的代码即使经过简单的合并压缩,其整体语义与函数名也是一一对应的。但 Vue 和 React 的源码必须经过一条现代化的构建流水线才能变成浏览器能够运行的代码。构建流水线主要包含以下四个核心操作步骤:
- 语法转译与宏替换:浏览器引擎不支持直接执行
<div>{name}</div>这样的 JSX 语法,Babel 或 SWC 会将其转译为_jsx('div', { children: name })或React.createElement函数调用;Vue 的编译器则会把<template>转换成高效的render渲染函数,并将静态节点标记为常量进行提升。 - 模块依赖解析与图谱构建:构建器(Vite、Rollup、Webpack)从应用的入口文件(通常是
main.js或index.tsx)开始,深度遍历所有import与export语句,在内存中构建起完整的依赖关系拓扑图。 - Tree-shaking(摇树优化)与代码分割:构建工具根据静态模块分析,将那些被引入但从未被实际调用的导出函数彻底剥离;同时根据路由配置把不同页面的组件代码拆分为独立的异步 Chunk,避免首屏一次性加载整站代码。
- 代码混淆与哈希指纹注入:为了在生产环境中实现长期缓存,构建器会重命名变量与函数(如将长变量名变为单字母
a、b),移除所有注释和多余空白,并在生成的文件名中嵌入内容哈希值(如chunk-vendors.8b3a1c.js)。
四、模块化、作用域与长期维护性差异
除了代码本身的运行方式,两种技术在工程化组织与多人协作上也体现出了根本的源码差异:
1. 作用域隔离与样式污染
Vanilla JS 在默认情况下所有声明在顶层的变量和类都属于全局作用域(除非严格使用 IIFE 闭包或原生 ESM 模块规范)。样式更是全局生效的,如果在 A 模块写了一个 .btn 样式,很容易无意中覆盖 B 模块的按钮样式。为了避免样式污染,原生开发者往往必须依赖严格的人工命名规范(例如 BEM 规范 block__element--modifier)。
在 Vue 或 React 源码中,作用域隔离是内置的基础设施。Vue 单文件组件提供了 <style scoped>,在编译时会自动为对应的 HTML 元素打上独一无二的属性指纹(如 data-v-7ba5bd90),并在 CSS 选择器后加上属性约束;React 则广泛使用 CSS Modules 或 CSS-in-JS(如 styled-components),编译后类名会被转换为形如 _Button_1x9kz_4 的哈希类名,彻底消除了全局命名冲突的隐患。
2. 数据流与跨组件通信
当一个网站包含数十个交互区域,且这些区域需要共享同一种状态(例如用户的登录信息、购物车清单)时:
- Vanilla JS:源码中往往需要自行维护一套观察者模式(EventEmitter / EventBus),或者将数据挂载在
window.globalState上。由于缺乏强约束的数据流规范,任何一个函数都可以随时篡改全局状态,当数据异常时,排查谁修改了数据就像在大海捞针。 - Vue / React:生态中拥有标准化、单向流动的状态管理机制(如 Pinia、Zustand、Redux Toolkit)。所有的状态变更在源码中都必须遵循明确的流程触发,配合浏览器开发者工具的时间旅行调试功能,可以清晰记录每一步操作引起的源码状态切片。
五、选型依据与特征对比全景
通过下表可以直观对比两者在各维度的技术表现与源码特征:
| 对比维度 | Vanilla JS 原生方案 | Vue / React 现代框架方案 |
|---|---|---|
| 开发期源码格式 | 标准的 .html、.css、.js 静态文件 | .vue、.jsx、.tsx 组件,依赖 TypeScript/SCSS |
| 构建与转译依赖 | 无强依赖,通常直接保存即可在浏览器调试 | 强依赖 Node.js 与构建工具(Vite、Webpack、Rollup) |
| 浏览器交付产物 | 原始 HTML 包含完整标签,代码即结构 | 包含空白容器的 HTML + 打包混淆后的 JS/CSS Bundle |
| DOM 操作方式 | 命令式(逐一获取元素句柄,手动调用属性赋值) | 声明式(维护数据状态,框架自动执行虚拟 DOM 或响应式更新) |
| 首屏加载与体积 | 体积极小(零框架运行时开销),网络直连加载飞快 | 存在基础运行时开销(React/Vue 基础库 30KB~130KB 不等) |
| 复杂状态维护 | 缺乏标准范式,状态复杂时容易出现竞态与错乱 | 单向数据流与标准状态库,逻辑分层清晰规范 |
| 团队协同扩展性 | 依赖开发者高度自律的规范约束,大型项目易臃肿 | 强组件化与标准工程脚手架,适合中大型团队工业化协同 |
适合JavaScript初学者的实战项目:制作简易待办事项列表
2026-11-05 16:17:07
JS打开新标签页时怎么通过URL传递参数
2026-11-05 12:21:08
sigma.js 自定义节点样式:设置颜色、大小及标签显示的技巧
2026-11-05 09:55:00
JS 生成的统计图怎么导出为 PNG 图片或 PDF 文件?
2026-11-05 09:37:58
JavaScript事件处理实例:点击、输入与表单验证代码
2026-11-05 09:06:19








