Vue.js 是由尤雨溪开发的渐进式 JavaScript 框架。所谓"渐进式",是指 Vue 可以按需求逐步采用——从简单的页面增强(通过 CDN 引入)到复杂的单页应用(SPA)都可以优雅胜任。Vue 3 于 2020 年发布,采用 Proxy 重写了响应式系统,引入了 Composition API,性能提升 1.3-2 倍,同时原生支持 TypeScript。
一、Vue 的定位与渐进式理念
1.1 Vue 是什么,适合什么场景
渐进式采用路径:
[增强 HTML] CDN 引入 → 简单交互
↓
[组件化开发] Vue CLI / Vite → 局部组件
↓
[单页应用] Vue Router + Pinia → 完整 SPA
↓
[全栈框架] Nuxt.js → SSR / SSG / 服务端渲染
| 场景 | Vue 选择 | 说明 |
|---|---|---|
| landing page 交互 | CDN 引入 | 300KB gzip,无需构建工具 |
| 中型管理后台 | Vue + Vite + Pinia | 组件化 + 状态管理 |
| 全栈 Web 应用 | Nuxt.js | SSR + API 路由 + 自动路由 |
| 企业级 Design System | Vue + 组件库 | Element Plus / Vuetify |
| 跨平台应用 | Vue + NativeScript / uni-app | 一套代码多端运行 |
1.2 Vue vs React vs Angular:12 维度对比
| 维度 | Vue 3 | React 18 | Angular |
|---|---|---|---|
| 学习曲线 | ⭐ 低(模板语法接近 HTML) | 中(JSX + Hooks 概念多) | 高(TypeScript + Decorator + DI) |
| 性能 | ⭐ 优秀(编译时优化) | 优秀(Compiler 自动 memo) | 良好(变更检测策略) |
| 响应式 | Proxy(原生响应式) | 需要 useState + 手动优化 | RxJS + Zone.js |
| 包体积 | ⭐ 最小(~10KB runtime) | ~40KB | ~130KB |
| TypeScript | ✅ 原生支持 | 社区主导 | ✅ 一等公民 |
| 模板语法 | HTML 模板(声明式) | JSX(JavaScript) | HTML 模板 |
| 状态管理 | Pinia(Vue 官方推荐) | Zustand/Redux/Jotai | NgRx / 服务层 |
| 路由 | Vue Router(官方) | react-router(社区) | Router(官方) |
| CLI 工具 | Vite / Vue CLI | Vite / Next.js | Angular CLI |
| 生态成熟度 | ⭐ 丰富(Vue 中文社区最强) | ⭐ 最大(npm 包最多) | 企业级 |
| 国内采用 | ⭐ 极高(Element Plus / Ant Design Vue) | 高 | 低 |
| Server Side | Nuxt.js | Next.js / Remix | Angular Universal |
一句话选型建议:
- 追求开发速度和中文生态 → Vue
- 追求灵活性和最大生态 → React
- 追求企业级规范和强类型 → Angular
二、响应式系统:Vue 的灵魂
2.1 为什么 Vue 的响应式比 React 直觉
Vue 的响应式基于依赖追踪。当响应式数据变化时,Vue 自动知道哪些依赖了该数据的组件需要更新。
<script setup>
import { ref, reactive, computed } from 'vue'
// ref:基本类型的响应式包装
const count = ref(0)
console.log(count.value) // 必须使用 .value 访问
// reactive:对象的深度响应式
const user = reactive({ name: 'Alice', age: 25 })
console.log(user.name) // 直接访问,无需 .value
// computed:计算属性(自动缓存)
const doubleCount = computed(() => count.value * 2)
// doubleCount.value 自动追踪 count 的依赖
// watch:侦听器
watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`)
})
function increment() {
count.value++ // 修改触发更新
}
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
<p>Double: {{ doubleCount }}</p>
</template>
2.2 ref vs reactive:怎么选?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基本类型(string/number/boolean) | ref | reactive 不支持基本类型 |
| 对象(Object/Array) | reactive | 直接访问属性,代码更简洁 |
| 需要解构/展开的对象 | ref | reactive 解构会失去响应性 |
| 作为 props 传递 | ref | ref 是值包装,传递更安全 |
| 表单对象 | reactive | 大量属性访问时更方便 |
注意:reactive 解构会失去响应性:
// ❌ 失去响应性
const state = reactive({ count: 0, name: 'Alice' })
const { count } = state // 解构后 count 不再触发更新
// ✅ 使用 toRefs 保持响应性
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
// count.value 仍然是响应式的
2.3 深度响应式与性能
Vue 3 的 Proxy 实现天然支持深度响应式:
const state = reactive({
user: {
profile: {
name: 'Alice'
}
}
})
// 自动追踪深层嵌套
state.user.profile.name = 'Bob' // 自动触发依赖更新
shallowRef / shallowReactive:当不需要深度响应式时提升性能:
import { shallowRef } from 'vue'
// 大型对象不需要深度响应时
const bigData = shallowRef({
list: [/* 10000 items */]
})
// 只有 bigData.value = newObj 会触发更新
// bigData.value.list[0] = 'x' 不会自动触发
三、模板语法:声明式 UI 的优雅
3.1 插值与指令
<template>
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 一次性插值(不追踪变化) -->
<p v-once>{{ staticMessage }}</p>
<!-- 原始 HTML(XSS 风险,谨慎使用) -->
<div v-html="rawHtml"></div>
<!-- 属性绑定 -->
<img :src="imageUrl" :alt="description">
<button :disabled="isLoading">Submit</button>
<!-- 类名绑定:对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 类名绑定:数组语法 -->
<div :class="[baseClass, isActive ? 'active' : '']"></div>
<!-- 条件渲染 -->
<div v-if="isAdmin">Admin Panel</div>
<div v-else-if="isModerator">Moderator Panel</div>
<div v-else>User Panel</div>
<!-- 列表渲染 -->
<ul>
<li v-for="(item, index) in list" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
<!-- 事件处理 -->
<button @click="handleClick">Click</button>
<input @input="e => search(e.target.value)">
<form @submit.prevent="onSubmit"></form>
<!-- 双向绑定 -->
<input v-model="searchText">
<!-- 等价于:<input :value="searchText" @input="searchText = $event.target.value"> -->
</template>
3.2 v-model:双向绑定的多种形态
<script setup>
const text = ref('')
const agree = ref(false)
const selected = ref('')
const checked = ref([])
</script>
<template>
<!-- 文本 -->
<input v-model="text">
<!-- 复选框 -->
<input type="checkbox" v-model="agree"> Agree
<!-- 单选组 -->
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
<!-- 多选组 -->
<input type="checkbox" v-model="checked" value="apple"> Apple
<input type="checkbox" v-model="checked" value="banana"> Banana
<!-- 自定义组件 v-model -->
<CustomInput v-model="text" />
<!-- 自定义组件内部:defineModel() 或 props + emit('update:modelValue') -->
</template>
四、Composition API:Vue 3 的核心革新
4.1 为什么需要 Composition API?
Options API 的问题是逻辑碎片化:
// ❌ Options API:一个功能的代码分散在 data/computed/methods 中
export default {
data() { return { query: '', results: [], loading: false } },
computed: { filtered() { /* ... */ } },
methods: {
search() { /* ... */ },
debounceSearch() { /* ... */ },
},
watch: { query() { /* ... */ } },
created() { /* ... */ },
}
Composition API 按逻辑关注点组织代码:
// ✅ Composition API:一个功能的代码聚合在一起
<script setup>
import { ref, watch, onMounted } from 'vue'
// ========== Search Feature =============
function useSearch() {
const query = ref('')
const results = ref([])
const loading = ref(false)
async function search() {
loading.value = true
results.value = await fetchApi(query.value)
loading.value = false
}
watch(query, debounce(search, 300))
onMounted(search)
return { query, results, loading }
}
// ========== Auth Feature =============
function useAuth() { /* ... */ }
// ========== Component =============
const { query, results, loading } = useSearch()
const { user, login, logout } = useAuth()
</script>
4.2 自定义 Composable:Vue 3 的核心复用模式
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 使用
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
Composable 命名约定:
- 文件名:
useXXX.ts - 函数名:
useXXX() - 返回对象(方便解构)
- 可接受可选参数
- 内部自动处理生命周期(onMounted/onUnmounted)
五、生命周期钩子
| Options API | Composition API (<script setup>) | 说明 |
|---|---|---|
beforeCreate | — | setup 替代 |
created | — | setup 替代 |
beforeMount | onBeforeMount | DOM 挂载前 |
mounted | onMounted | DOM 已挂载 |
beforeUpdate | onBeforeUpdate | 更新前 |
updated | onUpdated | 更新后 |
beforeUnmount | onBeforeUnmount | 卸载前 |
unmounted | onUnmounted | 已卸载 |
| — | onErrorCaptured | 捕获后代错误 |
| — | onActivated / onDeactivated | KeepAlive |
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('Component mounted')
})
onUnmounted(() => {
console.log('Component unmounted')
})
</script>
六、现代特性:Teleport 与 Suspense
6.1 Teleport:DOM 跨层级挂载
<template>
<div class="page">
<!-- Modal 被渲染到 body 下,而非 .page 内 -->
<Teleport to="body">
<div class="modal" v-if="showModal">
<h2>Modal Title</h2>
<p>Modal content...</p>
</div>
</Teleport>
</div>
</template>
适用场景:Modal、Toast、Tooltip 等需要脱离当前 DOM 层级的组件。
6.2 Suspense:异步组件等待
<script setup>
import { defineAsyncComponent, Suspense } from 'vue'
const AsyncChart = defineAsyncComponent(() => import('./Chart.vue'))
</script>
<template>
<Suspense>
<template #default>
<AsyncChart />
</template>
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
七、Vue 生态系统速查
| 领域 | 主流方案 | 说明 |
|---|---|---|
| 路由 | Vue Router 4 | 声明式路由,支持组合式 API |
| 状态管理 | Pinia | Vue 官方推荐,TypeScript 友好 |
| 构建工具 | Vite | 原生 ESM,HMR 极速 |
| UI 组件库 | Element Plus / Ant Design Vue / Vuetify | 企业级组件库 |
| 全栈框架 | Nuxt.js 3 | SSR/SSG/API 路由 |
| 移动端 | uni-app / NativeScript | 跨平台方案 |
| 测试 | Vitest + Vue Test Utils | 官方测试库 |
| 表单 | VeeValidate / FormKit | 表单验证与处理 |
常见问题(FAQ)
Vue 2 迁移到 Vue 3 需要做什么?
详见 Vue3 和 Vue2 的区别。核心变化:
- 响应式从
Object.defineProperty→Proxy - Options API → Composition API(可选迁移)
- Vue Vuex → Pinia
- Event Bus(
$on/$off)移除 →mitt库替代 - 生命周期钩子重命名
ref 为什么需要 .value?
因为 Vue 需要区分"基本类型值的包装"和"对象引用"。JavaScript 只有一个值可以同时支持基本类型和对象,就是通过 object 包装。.value 是访问底层值的标准方式。在 <template> 中不需要 .value,因为 Vue 自动解包。
computed 和 method 有什么区别?
computed 有缓存——依赖不变时不会重新计算;method 每次调用都会执行。需要缓存的结果用 computed,不需要缓存的副作用用 watch。
Vue 会被 React 淘汰吗?
短期内不会。Vue 在中国和亚洲市场拥有庞大的开发者基础和中文文档优势。Vue 3 + Vite 的组合在很多场景下开发效率高于 React。两个框架会长期共存。
相关阅读
- Vue Composition API 完全指南 — setup、ref、reactive、computed、watch、生命周期
- Vue Router 完全指南 — 声明式路由、导航守卫、动态路由、懒加载
- Pinia 状态管理指南 — Pinia vs Vuex、Store 设计、持久化
- Vue + TypeScript 实战指南 — 组件类型、Props 泛型、API 数据契约
- Vue 测试深度指南 — Vitest + Vue Test Utils + Playwright
- Nuxt.js 完全指南 — SSR、SSG、API 路由、全栈开发
- Vue 性能优化指南 — 虚拟滚动、懒加载、渲染优化
- React 详解 — Vue 与 React 的核心差异对比
- Tailwind CSS 详解 — Vue + Tailwind 开发模式
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。