理解 Vue 的运行机制
理解 Vue 的运行机制
概述
在 Web 开发的早期,网页大多是静态的:HTML 构建骨架,CSS 妆点外观,JavaScript 仅用于实现简单的交互逻辑。然而,随着现代应用对实时性的要求越来越高——例如股票看板、实时监控数据——我们需要一种机制,让数据变化能够实时、自动地映射到界面上。
在桌面应用开发(如 WPF)中,我们习惯了数据绑定带来的便利。Vue.js 正是 Web 前端的“WPF 运行时”:它是一个渐进式的动态框架,通过声明式的数据绑定,让我们专注于数据逻辑,而将繁琐的 DOM 操作交给框架处理。
这篇博客通过介绍脚本引入和构建工具两种使用 Vue 的方式,厘清 Vue 的运行机制。
脚本引入模式
Vue 示例
Vue 的使用方式非常灵活。现代项目通常使用构建工具(如 Vite, Webpack),但在学习原理时,最直观的方式是直接通过 <script> 标签引入。这种方式不需要编译,浏览器直接执行,非常适合理解“运行时”发生了什么。
下面是一个混合了“传统 JS 操作”与“Vue 接管区域”的示例:
1 |
|
运行图景:浏览器中发生了什么?
第一阶段:浏览器原生解析
- HTML 解析:浏览器从上到下解析 HTML。它看到了
<div id="app">和里面的<my-panel>。 - 陌生标签:此时,浏览器并不认识
<my-panel>,它只是将其作为一个普通的自定义元素保留在 DOM 树中,界面上不会显示该组件的内容,或者显示为空白。 - 脚本加载:浏览器加载 Vue 的库文件,然后执行底部的
<script>代码。
第二阶段:Vue 的挂载 (Mounting)
当代码执行到 app.mount('#app') 时,Vue 正式介入。这句话的意思是:“找到 id 为 app 的元素,从此以后,这里面的内容归 Vue 管。”
Vue 内部会执行以下流程:
- 接管容器:Vue 锁定
#app元素。 - 解析模板:Vue 扫描容器内的 HTML,发现了
<my-panel>标签。 - 组件实例化:Vue 根据注册表中
my-panel对应的MyPanel对象,创建组件实例。 - 初始化响应式:运行组件的
setup()函数,创建ref数据,建立依赖追踪系统。 - 渲染 (Render):
- Vue 将组件的
template编译为渲染函数。 - 执行渲染函数,生成 虚拟 DOM (Virtual DOM)。
- 根据虚拟 DOM,生成真实的 HTML 结构。
- Vue 将组件的
- 替换与显示:Vue 用生成的真实 DOM 替换掉页面上原本的
<my-panel>占位符。
此时,用户看到了计数器和传感器数值。
1 | app.mount() |
为了更好地理解上述过程,我们可以用 WPF 桌面应用开发做类比。Vue 其实就是运行在浏览器里的“渲染引擎 + 框架运行时”。
| 概念 | WPF (桌面端) | Vue (Web 端) | 作用 |
|---|---|---|---|
| View | XAML | Template (HTML) | 声明界面的结构和外观 |
| ViewModel | ViewModel Class | setup() / Script | 只有逻辑和数据,不直接操作 UI |
| Data Binding | {Binding Prop} |
{{ ref }} |
建立数据与 UI 的连接 |
| App Host | Application / Window | createApp() / Root Component | 应用的容器和入口 |
| Runtime | .NET CLR | Browser JS Engine + Vue | 负责代码执行和内存管理 |
Vue 相关概念
应用与组件的关系
在代码中,我们先创建了 app,然后注册了 component。
1 | const app = createApp({}) |
app 是组件的容器,组件不是全局存在的,它必须依赖一个 Vue 应用。就像 WPF 里先有 Application / Window(根),再往里面加控件(UIElement → Button、Grid 等)。app 和组件构建了一个树状结构:
- App (应用实例):全局上下文。一个页面通常只有一个 App 实例。它管理着所有的全局配置(如路由、状态管理 Pinia、全局组件)。
- Component (组件):UI 的积木。组件必须依附于 App 存在(或者作为其他组件的子组件)。
1 | App (Root) |
这种结构保证了数据流向清晰,且组件之间相互隔离,通过明确的接口(Props/Events)通信。
setup():组件的构造函数
setup() 是 Vue 3 Composition API 的入口。它相当于 ViewModel 的构造函数。它的职责非常纯粹:
- 定义数据:使用
ref或reactive声明需要被追踪的数据。 - 定义行为:设置定时器、发起网络请求、监听事件。
- 暴露接口:通过
return将数据和方法暴露给模板。
我们可能好奇的是,为什么这个 ViewModel 必须把要更新绑定的变量放到 return 中?
1 | setup() { |
这是由 JavaScript 的作用域(Scope)决定的。 * setup 只是一个普通的 JavaScript 函数。 * counter 是这个函数内部的局部变量。 * HTML 模板(Template)在编译后,会变成一个渲染函数。这个渲染函数运行在组件实例的上下文中。 * 如果不 return,渲染函数就看不见 counter,也就无法把它显示在界面上。
return 的动作,实际上是将局部变量“挂载”到了组件实例上,让模板引擎能够访问到它。
Ref:响应式的魔法包裹
Vue 的 ref(0) 并不是简单地返回 0,而是返回了一个对象:
1 | // ref(0) 的概念结构 |
- 在 JS 中:我们需要通过
.value来读写数据(counter.value++)。这是因为 JavaScript 的原始类型(数字、字符串)是值传递的,如果直接返回数字0,Vue 就无法追踪它的变化了。我们需要一个对象作为“引用”容器。 - 在 模板 中:Vue 会自动帮我们“解包”。所以我们在 HTML 里只需要写
{{ counter }},而不需要写{{ counter.value }}。
当 counter.value 改变时,Vue 内部的通知机制(类似于 WPF 的 PropertyChanged)会被触发,通知虚拟 DOM 重新计算,进而更新界面。
语言基础
对象解构赋值和对象字面量
在 MyPanel 组件的 setup() 方法中,const { ref, onMounted } = Vue; 是 JavaScript 的对象解构赋值,意思就等价于:
1 | const ref = Vue.ref; |
这里的 const MyPanel = { ... } 中的 {} 是对象字面量语法,它可以快速地定义一个对象:
1 | var server = { |
也有方法的简写语法糖形式:
1 | const server = { |
JavaScript 异步模型
代码中使用了 setInterval 和回调函数,这触及了 JavaScript 最核心的并发模型:单线程 + 事件循环。
1 | setInterval(() => { |
JavaScript 在浏览器中只有一个主线程。它不能像 C# 那样开启多线程来处理后台任务。setInterval 并没有阻塞代码,它只是向浏览器注册了一个“闹钟”。当 1000ms 到达时,浏览器把回调函数扔进“任务队列”。一旦主线程空闲下来,就会从队列里取出这个函数执行。这种机制使得 Vue 组件可以在等待数据(如 fakeSensor 模拟的网络请求)时,依然保持界面响应,不会卡死浏览器。
比如在我们示例中未使用 Vue 的部分,就直接用了异步回调操纵 DOM 来实现动态时间的展示。但是这造成了逻辑和页面(HTML)元素之间的耦合,这其实也是 MVVM 架构想要解决的依赖问题。
Vite 构建:现代化的 Vue 工程结构
基本图像:从单文件到模块化
刚才我们用纯 HTML + 脚本引入的方式演示了 Vue 的核心流程。实际上,使用 Vite 等构建工具开发 Vue 项目,底层逻辑与之前并无二致,区别在于代码的组织方式。
在“脚本引入”模式下,所有逻辑(HTML 模板、CSS 样式、JS 逻辑)往往挤在一个 HTML 文件里,难以维护。而在“工程化”模式下,我们将 App、各个组件以及入口 index.html 拆分到不同的文件中,实现了模块化管理。
我们可以使用 Vite 快速初始化一个标准的 Vue 项目:
1 | # 1. 创建项目 |
启动后,浏览器打开提示的本地地址(通常是 http://localhost:5173),你会看到一个运行中的 Vue 页面。
开发 vs 生产
这里的开发服务器(Dev Server)主要用于热更新 (HMR):当你修改代码保存时,浏览器里的页面会瞬间局部刷新,无需手动 F5。
而在实际部署上线时,我们不会把源码上传服务器,而是执行构建命令:
1 | npm run build |
这个命令会将 Vue 项目编译、压缩成最原始的 HTML + CSS + JavaScript 静态文件(通常输出在 dist 目录)。这些文件更加轻量、兼容性更好,你可以直接把它们丢到 Nginx、Apache 或任何静态文件服务器上运行。
工具链图谱:npm、Vite 与 Vue
初学者容易混淆这几个工具的关系,我们可以这样理解:
graph TD
npm[npm: 仓库管理员] -->|安装依赖/运行脚本| Vite[Vite: 工厂与运输队]
Vite -->|编译/热更新| Vue[Vue: 原材料与蓝图]
- npm (Node Package Manager):它是管家。它不关心 Vue 或 Vite 具体是怎么工作的,它只负责根据
package.json的清单,把需要的工具包下载到node_modules里,并帮你运行定义好的脚本(如npm run dev)。 - Vite (Build Tool):它是现代化的构建工厂。
- 开发时:它启动一个本地服务器,利用浏览器原生的 ESM 能力(下文会讲),实现秒级启动和热更新。
- 构建时:它负责编译
.vue文件、转换 TypeScript、压缩代码、处理 CSS 模块化,最终产出浏览器能直接运行的静态资源。
- Vue (UI Framework):它是核心规则。我们按照它的语法规范编写组件(
<template>,setup),它提供响应式系统和组件化能力。
当你执行 npm run dev 时,流程如下: 1. npm 读取 package.json 中的 "scripts": { "dev": "vite" }。 2. npm 启动 Vite。 3. Vite 启动开发服务器,拦截浏览器请求。 4. 浏览器请求文件时,Vite 实时编译 .vue 组件并返回给浏览器。 5. Vue 在浏览器中运行,渲染界面。
运行顺序与入口解析
工程化后,项目的入口依然是 index.html(通常位于根目录),但它的内容变得非常干净:
1 |
|
什么是 type="module"?
注意看这行代码:<script type="module" src="/src/main.js"></script>。
这是现代浏览器支持的 ESM (ECMAScript Modules) 标准。 * 在过去,浏览器里的 JS 无法直接相互引用,必须把所有代码打包成一个大文件或者按顺序加载多个 script 标签。 * 有了 type="module",浏览器就看懂了 JavaScript 里的 import 和 export 语法。 * 这意味着浏览器看到 import { createApp } from 'vue' 时,会主动去请求对应的文件。Vite 正是利用了这个特性,在开发环境下不打包,直接让浏览器按需请求文件,所以启动速度极快。
代码迁移:从脚本到模块
Vite 将逻辑的指挥棒交给了 /src/main.js。让我们看看它里面发生了什么,你会发现它和我们在上面写的 <script> 内容几乎一样,只是被拆分了:
1 | // src/main.js |
这就是 “真实生产项目的基本骨架”:index.html 提供容器、main.js 负责启动、.vue 文件负责具体的 UI 和逻辑。
单文件组件 (SFC)
现在我们来看核心的变化:.vue 文件。这种文件格式被称为 SFC (Single File Component),即“单文件组件”。它把一个组件所需的 HTML(结构)、JavaScript(逻辑)和 CSS(样式)封装在一个文件里。
我们需要定义根组件 src/App.vue:
1 | <template> |
接着是具体的业务组件 src/components/MyPanel.vue:
1 | <!-- 1. 模板区 (Template) --> |
浏览器并不认识 .vue 文件。当我们在 main.js 中 import App from './App.vue' 时,Vite(借助插件 @vitejs/plugin-vue)会在幕后把这个 .vue 文件“翻译”成浏览器能看懂的 JS 对象: * <template> 变成了渲染函数。 * <script setup> 变成了 setup() 函数。 * <style> 被提取并插入到页面的 <head> 中。
总结:Vue 应用的完整生命周期
经过前两部分的探索,我们已经从最简单的 <script> 标签引入,进阶到了使用 Vite 进行模块化构建。现在,让我们跳出代码细节,建立一个 Vue 应用从开发到部署再到用户浏览器运行的完整宏观图像。
最终产物:一切回归静态
无论我们在开发时写了多么复杂的 .vue 组件,用了多么高级的 TypeScript 语法或 CSS 预处理器,最终交给浏览器的,永远只有“网页三剑客”:HTML、CSS 和 JavaScript。
当执行 npm run build 命令时,Vite 会扮演“翻译官”和“打包工”的角色: 1. 翻译:把 .vue 文件里的 <template> 编译成 JS 渲染函数。 2. 打包:把成百上千个模块文件合并、压缩成几个体积极小的 JS 文件。 3. 输出:将所有结果放入 dist(发布)目录。
最终,dist 目录里的结构通常是这样的:
1 | dist/ |
这就是 Vue 应用的物理本质:一堆静态文件。
托管与部署:不需要后端逻辑
因为产物是纯静态文件,所以部署 Vue 应用非常简单。不需要像 ASP.NET Core 或 Java Spring Boot 那样运行一个复杂的后端服务程序。
只需要一个简单的静态文件服务器(Web Server): * Nginx:业界最常用的高性能 HTTP 服务器。 * Apache:老牌服务器。 * CDN / 云存储:如 AWS S3, Vercel, Netlify, 阿里云 OSS。
只需要把 dist 文件夹里的内容上传上去,配置 Web Server 指向 index.html,部署就完成了。
运行时图像:CSR(客户端渲染)
那么,一个静态的 HTML 文件,是如何变成动态的、实时更新的应用程序的呢?这个过程被称为 SPA(单页应用) 或 CSR(客户端渲染)。
让我们看一遍当用户访问网址时发生的完整流程:
第一步:下载“空壳”
用户在浏览器输入网址。Nginx 返回 dist/index.html。
此时,用户看到的只是一个白屏或加载动画,因为 HTML 里只有一个空的 <div id="app">。
第二步:加载“大脑”
浏览器解析 HTML,发现 <script src="assets/index.js">,开始下载这个巨大的 JS 文件。这个文件里包含了 Vue 的引擎代码和写的 setup() 逻辑。
第三步:激活(Hydration / Mount)
JS 下载并执行。 1. 启动:createApp(App).mount('#app') 被执行。
接管:Vue 找到页面上的
#app节点。渲染:Vue 根据逻辑生成虚拟 DOM,并转换成真实的 DOM 元素(输入框、按钮、表格等)塞进
#app里。瞬间,页面从空白变成了丰富多彩的应用界面。
第四步:动态更新
从此之后,页面基本不再需要刷新。 * 数据变化:当定时器触发或用户点击按钮,你的 ref 数据发生变化。 * 响应式反应:Vue 监听到变化,重新计算虚拟 DOM,找出差异。 * 局部修补:Vue 只修改变动部分的真实 DOM(例如只修改了 <span> 里的数字)。
全景图
最后,我们将这个过程总结为一张图:
graph TD
subgraph 开发阶段 [Developer Machine]
Code[.vue / .js] -->|npm run build| Compiler[Vite 构建]
Compiler --> Artifacts["dist 目录 (HTML+JS+CSS)"]
end
subgraph 部署阶段 [Web Server / Nginx]
Artifacts -->|上传| Server[静态文件托管]
end
subgraph 运行阶段 [User Browser]
User[用户访问 URL] -->|请求| Server
Server -->|返回 index.html + JS| Browser[浏览器]
Browser -->|执行 JS| VueEngine[Vue 运行时启动]
VueEngine -->|生成 DOM| Screen[显示界面]
Event[用户交互/定时器] -->|修改 ref| Reactivity[响应式系统]
Reactivity -->|Patch| Screen
end