Vue 3 不是「Vue 2 加几个 API」,而是一次从响应式内核到编译策略的重写。理解两版差异的核心切入点是响应式原理与组件代码组织方式,其余破坏性变更大多围绕这两点展开。Vue 3 于 2020 年 9 月正式发布,Vue 2 已于 2023 年 12 月 31 日停止维护,新建项目应直接使用 Vue 3(推荐 <script setup> 写法)。
Vue 2 用 Object.defineProperty 给对象的每个属性逐一劫持 getter/setter;Vue 3 改用 ES6 Proxy 代理整个对象。这是两版最根本的差别,直接决定了「新增属性」「数组索引修改」「Map/Set」等场景是否响应式。详见 Proxy 与 Reflect 词条。
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 劫持机制 | Object.defineProperty 逐属性重写 getter/setter | Proxy 代理整个对象,13 种 trap 拦截 |
| 新增 / 删除属性 | 非响应式,需 Vue.set / Vue.delete(this.$set) | 天然响应式,直接赋值即可 |
| 数组监听 | 重写 7 个变更方法(push、pop、splice 等),索引赋值不触发 | 索引赋值、length 修改均可触发 |
| Map / Set 支持 | 不响应式 | 原生响应式(由 reactive 实现) |
| 初始化成本 | 递归遍历所有属性,深层对象一次性全部劫持 | 懒代理,访问到深层属性才继续代理,初始化更快 |
| 性能 | 大对象初始化慢;无法监听属性新增 | 初始化与更新性能均更好,约为 Vue 2 的 1.3~2 倍(官方基准) |
| 浏览器要求 | IE 11 可用(需 es5 构建) | 不支持 IE 11,需 Proxy(Chrome 49+ / Edge 12+ / Firefox 18+ / Safari 10+) |
Vue 2 的选项式 API(Options API)按 data、methods、computed 分组组织代码,同一业务逻辑被拆散到各选项;Vue 3 引入组合式 API,把同一逻辑关注点的状态、方法、计算属性聚拢到一个函数里,大组件的可维护性显著改善。详见 组合式 API。
| 对比维度 | Vue 2(选项式 API) | Vue 3(组合式 API) |
|---|---|---|
| 代码组织 | 按选项类型分组,同类逻辑散落各处 | 按功能分组,相关代码聚合成 setup 函数 / 组合式函数 |
| 逻辑复用 | mixin,存在命名冲突、来源不清的问题(见 mixin 混入) | 自定义组合式函数(useXxx),来源明确、无命名冲突 |
| this 指向 | 指向组件实例,可用 this 访问 route、store 等 | setup 中无 this,通过参数和导入获取上下文 |
| TypeScript 友好度 | 类型推断困难,需 vue-class-component / vue-property-decorator 装饰器 | 原生友好,严格模式下推断完整 |
| 学习曲线 | 平缓,模板写法直观 | 需理解 ref / reactive 与依赖收集,曲线略陡 |
| 兼容性 | 仅 Vue 2(Vue 2.7 向后移植了组合式 API) | Vue 3 原生;推荐 <script setup> 语法糖 |
最小代码示例:同一计数器组件的两种写法
<!-- Vue 2:选项式 API --> <script> export default { data() { return { count: 0 } }, computed: { double() { return this.count * 2 } }, methods: { add() { this.count++ } }, mounted() { console.log('mounted') } } </script> <!-- Vue 3:<script setup> 组合式 API --> <script setup> import { ref, computed, onMounted } from 'vue' const count = ref(0) // 响应式状态 const double = computed(() => count.value * 2) function add() { count.value++ } onMounted(() => console.log('mounted')) </script>
Vue 2 模板强制单一根节点,弹窗与异步组件都要写 hack;Vue 3 从编译器层面原生支持这三个能力。
| 能力 | Vue 2 | Vue 3 |
|---|---|---|
| 多根节点(Fragment) | 报错 "Component template should contain exactly one root element",需额外包裹元素 | 支持多个根节点,渲染为 Fragment,不影响 DOM 层级 |
| Teleport 传送门 | 弹窗需手动挂载到 body(Vue.extend + $mount) | <teleport to="body"> 声明式渲染到任意节点,见 Teleport |
| Suspense 异步组件 | 仅简单异步组件(factory + loading 分支),无统一 fallback | <suspense> 带 fallback 插槽,可配合异步 setup(),见 Suspense |
组合式 API 中,选项式生命周期改为以 on 开头的导入函数,并在 生命周期文档中给出了一一映射:
| Vue 2 选项式 | Vue 3 组合式 | 说明 |
|---|---|---|
| beforeCreate / created | setup() 本身 | setup 执行在这两个钩子之间,直接写业务逻辑即可 |
| beforeMount | onBeforeMount | 挂载前,DOM 未生成 |
| mounted | onMounted | 挂载完成,可操作 DOM |
| beforeUpdate | onBeforeUpdate | 响应式数据变更、DOM 打补丁前 |
| updated | onUpdated | DOM 打补丁后,避免在此修改状态 |
| beforeDestroy | onBeforeUnmount | 卸载前,清理定时器 / 事件监听(改名更贴切) |
| destroyed | onUnmounted | 卸载完成 |
| activated / deactivated | onActivated / onDeactivated | 配合 keep-alive 缓存组件 |
| errorCaptured | onErrorCaptured | 捕获后代组件错误 |
| —(新增) | onRenderTracked / onRenderTriggered | 调试用:精确追踪哪个依赖触发了重渲染 |
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 源码语言 | JavaScript,类型靠 vue-class-component / 装饰器模拟 | 用 TypeScript 重写,类型定义随源码生成,保持同步 |
| 组件 props 类型 | 需 PropType 断言,编辑器提示弱 | 基于字面量推导,类型推断完整到模板 |
| 模板检查 | volar 之前基本无模板类型检查 | Volar(Vue 官方 VS Code 插件)可对模板表达式做类型检查 |
| 脚手架默认 | Vue CLI 需手动勾选 TS | create-vue 默认可选 TS + <script setup lang="ts"> |
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 运行时大小(gzip) | vue.runtime.min.js 约 20KB | 约 12KB(tree-shaking 后按需引入) |
| Tree shaking | 整体引入,难以按需摇掉 | 编译器与运行时分离,API 具名导入可摇掉 |
| 重渲染机制 | 全量 diff(组件级) | 基于 虚拟 DOM 的静态提升 + PatchFlag,动态节点精准更新 |
| 编译优化 | 无静态标记 | 静态提升、缓存事件处理器、Block 树收集动态子节点 |
新建项目:无条件选 Vue 3 + Vite + <script setup>,Vue 2 已停止维护(2023-12-31 EOL)且不再收到安全更新。存量 Vue 2 项目:先升级到 Vue 2.7(官方向后移植了组合式 API 与 <script setup>),在新功能上试用组合式 API;配合 @vue/compat 兼容构建逐步迁移到 Vue 3。详见迁移清单章节与 Vue2 迁移 Vue3 词条。状态管理从 Vuex 迁到 Pinia,UI 库 element-ui 换 element-plus。
两者解决的都是「把源码变成浏览器可运行的产物」的问题,但路线相反:Webpack 是编译时打包——启动即把所有模块编译成一个 bundle;Vite 是运行时按需编译——开发期用浏览器原生 ESM,请求哪个模块才编译哪个。详见 Webpack 与 Vite 词条。
| 对比维度 | Webpack | Vite |
|---|---|---|
| 开发期工作方式 | 编译时把所有模块打包成 bundle,再启动 dev server | dev server 直接启动,借助浏览器原生 ESM 按请求编译模块 |
| 冷启动速度 | 与项目规模强相关,中大型项目几十秒到几分钟 | 与项目规模几乎无关,通常亚秒级启动 |
| 首屏加载(开发期) | 一个本地 bundle,首屏不再编译 | 按路由按需编译,首屏只编译用到的模块 |
| HMR 热更新 | 通过模块图反向推导受影响模块,热更新边界计算较重 | 通过 import.meta.hot 精确失效,配合 SWC/esbuild 速度接近原生 |
| 预构建 | 无此概念 | 用 esbuild 把 CommonJS 依赖预构建成 ESM(如 lodash、react) |
| 对比维度 | Webpack | Vite |
|---|---|---|
| 配置文件 | webpack.config.js,entry/output/loader/plugin/resolve 等众多概念 | vite.config.js,多数场景零配置开箱即用 |
| 资源处理 | 一切皆模块:css-loader、style-loader、babel-loader、file-loader 等 loader | 内置 CSS、静态资源、TS、JSX 处理,扩展靠 插件(rollup 插件兼容) |
| 生态成熟度 | 极其成熟:Module Federation、长缓存策略、增量编译、_DLL 等大量实践 | 快速成长,周边插件已覆盖主流场景,但深度定制能力不及 Webpack |
| 微前端 / 多入口 | Module Federation 原生支持,大型 MPA 常见配置 | 可通过插件实现,复杂场景需自行组合 |
| 适用团队 | 有历史包袱、需要精细控制产物的大型项目 | 新项目、追求开发体验与速度的中大型项目 |
| 对比维度 | Webpack | Vite |
|---|---|---|
| 底层打包器 | 自研打包器(基于 tapable 事件流) | Rollup(生产构建)+ esbuild/SWC(转译) |
| 代码分割 | splitChunks 配置强大但复杂 | 默认按动态 import 分包,配置简单 |
| 压缩 | terser / swc 压缩 | esbuild 或 terser(可选) |
| CSS 处理 | loader 链(MiniCssExtractPlugin + Autoprefixer) | 内置 PostCSS + Autoprefixer,CSS 代码分割开箱即用 |
| SSR / SSG | 需自行搭建或用框架(Next.js、Nuxt 2) | 原生 SSR API;Nuxt 3、VitePress、SvelteKit 均基于 Vite |
| 与 newer 工具关系 | Webpack 5 支持 Module Federation;Turbopack 定位其继任者 | 与 Turbopack、Rsbuild 互通生态 |
最小代码示例:同一项目的构建配置对比
// webpack.config.js(Vue 2 时代常见形态,约 40~80 行起步) const path = require('path'); module.exports = { mode: 'development', entry: './src/main.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] } ] }, devServer: { hot: true, port: 8080 } }; // vite.config.js(等价能力,通常 5~10 行) import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173 } // CSS、静态资源、 TS、 HMR 全部内置,无需配置 })
新项目:默认 Vite(Vue 用 create-vue、React 用 vite-plugin-react,Nuxt 3 / SvelteKit / VitePress 同生态)。存量 Webpack 项目:先用 Webpack 5 的持久化缓存与 SWC loader 提速;若改造成本可控再迁移 Vite,注意 Module Federation / 特殊 loader 依赖是否可替代。需要深度定制产物(如 Module Federation 微前端、多端 SDK 构建)时 Webpack 仍更稳。
这不是「框架 vs 无框架」的选择,而是两种 CSS 组织哲学:原子化(utility-first,把样式拆成单用途类)vs 语义化(类名表达业务含义,样式集中在样式表)。Tailwind v3 的 JIT 引擎与 v4 的 Lightning CSS 架构已把「包体积」痛点基本消除。详见 Tailwind 与 原子化 CSS 词条。
| 对比维度 | Tailwind CSS | 原生 CSS |
|---|---|---|
| 类名原子化 | px-4 py-2 rounded-lg 组合原子类,直接写在 HTML 上 | 自定义语义类名(如 .btn-primary),样式与结构分离 |
| 设计令牌 | 内置 spacing / color / font-size 体系,团队天然统一;通过 tailwind.config.js 扩展 | 需自行约定,常配合 CSS 变量与设计稿规范 |
| 响应式前缀 | md:flex、lg:grid-cols-3 一类即媒体查询 | 手写 @media 或容器查询块 |
| 暗色模式 | dark:bg-gray-900 前缀切换,依赖 prefers-color-scheme 或 class 策略(见 暗色模式) | 手写 dark 主题样式与变量切换逻辑 |
| 维护性 | 改样式不用跨文件,但 HTML 类名长、可读性见仁见智 | 结构干净,但样式扩散、命名(BEM 等)负担重,见 BEM |
| 学习成本 | 需熟悉一套约定式类名(约数百个常用类),有记忆门槛 | 零学习成本,但工程化规范要自己建 |
| 包体积 | JIT 按使用类名生成,最终通常 10~30KB;无未使用样式 | 取决于项目规模与清理力度,老项目常有 100KB+ 死代码 |
| 与组件框架搭配 | Vue / React 单文件组件中天然契合,配合 headless UI 生态 | 配合 CSS Modules / CSS in JS 保证隔离 |
| 调试体验 | 类名即样式,DevTools 直接可读;但样式来源分散 | 样式集中,可读性与覆盖清晰 |
最小代码示例:同一个按钮的两种写法
<!-- Tailwind:原子类直接写 --> <button class="bg-blue-600 text-white font-medium px-4 py-2 rounded-lg hover:bg-blue-700 dark:bg-blue-500 transition-colors"> 提交 </button> <!-- 原生 CSS:语义类 + 样式表 --> <button class="btn-primary">提交</button> /* styles.css */ .btn-primary { background: #2563eb; color: #fff; font-weight: 500; padding: .5rem 1rem; border-radius: .5rem; transition: background .15s; } .btn-primary:hover { background: #1d4ed8; } @media (prefers-color-scheme: dark) { .btn-primary { background: #3b82f6; } }
选 Tailwind:新项目、设计令牌尚未建立、多人协作需强约束样式一致性、大量重复 UI 组件场景。选原生 CSS:对包体积零依赖有要求(活动页、嵌入式小组件)、团队已有成熟命名规范(BEM)与 PostCSS 流水线、样式高度主题化需深度定制。折中方案:Tailwind 做布局与原子能力,复杂业务组件用 CSS Modules 局部覆盖;也可以评估 UnoCSS、WindiCSS 等同类方案。若只是不想要预处理器,现代 CSS 嵌套 + 容器查询 + CSS 变量已覆盖大部分原生书写需求。
<script type="module" src="/src/main.js">(Vite 以 index.html 为入口)