Vue 详解:渐进式框架的核心概念与响应式系统

Vue.js 是渐进式 JavaScript 框架,以声明式渲染、响应式系统和组件化架构著称。本文系统讲解 Vue 3 的核心概念:Options API vs Composition API、响应式原理(Proxy + ref/reactive)、模板语法、计算属性与侦听器、生命周期、Teleport/Suspense、以及 Vue 与 React/Angular 的对比选型。

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.jsSSR + API 路由 + 自动路由
企业级 Design SystemVue + 组件库Element Plus / Vuetify
跨平台应用Vue + NativeScript / uni-app一套代码多端运行

1.2 Vue vs React vs Angular:12 维度对比

维度Vue 3React 18Angular
学习曲线⭐ 低(模板语法接近 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/JotaiNgRx / 服务层
路由Vue Router(官方)react-router(社区)Router(官方)
CLI 工具Vite / Vue CLIVite / Next.jsAngular CLI
生态成熟度⭐ 丰富(Vue 中文社区最强)⭐ 最大(npm 包最多)企业级
国内采用⭐ 极高(Element Plus / Ant Design Vue)
Server SideNuxt.jsNext.js / RemixAngular 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)refreactive 不支持基本类型
对象(Object/Array)reactive直接访问属性,代码更简洁
需要解构/展开的对象refreactive 解构会失去响应性
作为 props 传递refref 是值包装,传递更安全
表单对象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>

  <!-- 原始 HTMLXSS 风险谨慎使用 -->
  <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 APIComposition API (<script setup>)说明
beforeCreatesetup 替代
createdsetup 替代
beforeMountonBeforeMountDOM 挂载前
mountedonMountedDOM 已挂载
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeUnmountonBeforeUnmount卸载前
unmountedonUnmounted已卸载
onErrorCaptured捕获后代错误
onActivated / onDeactivatedKeepAlive
<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
状态管理PiniaVue 官方推荐,TypeScript 友好
构建工具Vite原生 ESM,HMR 极速
UI 组件库Element Plus / Ant Design Vue / Vuetify企业级组件库
全栈框架Nuxt.js 3SSR/SSG/API 路由
移动端uni-app / NativeScript跨平台方案
测试Vitest + Vue Test Utils官方测试库
表单VeeValidate / FormKit表单验证与处理

常见问题(FAQ)

Vue 2 迁移到 Vue 3 需要做什么?

详见 Vue3 和 Vue2 的区别。核心变化:

  • 响应式从 Object.definePropertyProxy
  • Options API → Composition API(可选迁移)
  • Vue Vuex → Pinia
  • Event Bus($on/$off)移除 → mitt 库替代
  • 生命周期钩子重命名

ref 为什么需要 .value

因为 Vue 需要区分"基本类型值的包装"和"对象引用"。JavaScript 只有一个值可以同时支持基本类型和对象,就是通过 object 包装。.value 是访问底层值的标准方式。在 <template> 中不需要 .value,因为 Vue 自动解包。

computedmethod 有什么区别?

computed 有缓存——依赖不变时不会重新计算;method 每次调用都会执行。需要缓存的结果用 computed,不需要缓存的副作用用 watch

Vue 会被 React 淘汰吗?

短期内不会。Vue 在中国和亚洲市场拥有庞大的开发者基础和中文文档优势。Vue 3 + Vite 的组合在很多场景下开发效率高于 React。两个框架会长期共存。

相关阅读

← 上一篇

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章