Vue.js 和 Vue 3 全面详解指南
1. Vue.js 基础介绍
1.1 什么是 Vue.js
Vue.js(简称 Vue)是一个用于构建用户界面的渐进式 JavaScript 框架。与其他框架不同,Vue 被设计为可以逐步采用。Vue 的核心库只关注视图层,易于上手,便于与其他库或既有项目整合。
Vue 由尤雨溪(Evan You)在 2014 年创建。尤雨溪曾在 Google 工作,参与了 AngularJS 的开发,后来他希望提取 Angular 中他认为精华的部分,构建一个更轻量级的框架,于是创建了 Vue。
1.2 Vue.js 的特点
-  渐进式框架:可以逐步将 Vue 集成到项目中,无需一次全部采用。 
-  响应式系统:Vue 提供了响应式且组件化的视图组件,当数据变化时,视图会自动更新。 
-  虚拟 DOM:Vue 使用虚拟 DOM(Virtual DOM)技术提高渲染性能。 
-  组件化开发:鼓励将应用拆分为独立可复用的组件,构建出大型应用。 
-  轻量级:Vue 的体积小巧,压缩后仅约 20KB(Vue 2)或 10KB(Vue 3)。 
-  易学易用:相较于其他前端框架,Vue 的学习曲线更平缓,API 设计简单直观。 
-  丰富的工具链:Vue 提供了完整的开发工具链,如 Vue CLI、Vite、DevTools 等。 
1.3 Vue.js 应用场景
Vue 可适用于多种场景:
-  单页应用(SPA):使用 Vue Router 构建单页应用,避免页面刷新提升用户体验。 
-  多页应用:将 Vue 组件集成到传统的多页面应用中。 
-  移动端应用:结合 Cordova、Capacitor 或 NativeScript 构建移动应用。 
-  桌面应用:结合 Electron 构建桌面应用。 
-  服务端渲染:使用 Nuxt.js 或手动配置 Vue SSR。 
-  静态站点生成:使用 VuePress 或 Gridsome 生成静态站点。 
1.4 Vue.js 版本历史
- Vue 1.0:2015 年 10 月发布,奠定了 Vue 的基础架构。
- Vue 2.0:2016 年 9 月发布,引入虚拟 DOM,提升性能。
- Vue 2.6:2019 年 2 月发布,添加了 Composition API RFC 等特性。
- Vue 3.0:2020 年 9 月发布,全新的架构,更好的性能和更小的体积。
- Vue 3.2:2021 年 8 月发布,引入 <script setup>语法。
- Vue 3.3:2023 年 5 月发布,改进了 TypeScript 支持和宏性能。
- Vue 3.4:2023 年 12 月发布,改进了渲染器和编译器性能。
2. Vue 2 与 Vue 3 比较
2.1 核心架构变化
| 特性 | Vue 2 | Vue 3 | 
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy | 
| 代码组织 | Options API 为主 | Options API + Composition API | 
| 模板编译 | 模板编译为渲染函数 | 改进的模板编译策略,更好的静态提升 | 
| 虚拟 DOM | 基本实现 | 重写,更快的挂载和更新速度 | 
| TypeScript 支持 | 有限支持 | 完全支持,代码库用 TS 重写 | 
| Tree-Shaking | 有限支持 | 全面支持,更小的打包体积 | 
2.2 API 变化
Vue 3 新增的 API:
- Composition API(setup,ref,reactive等)
- Teleport 组件
- Fragments(片段)
- Suspense 组件
- createApp替代- new Vue()
- 多个根节点支持
- emits选项
Vue 3 移除的 API:
- $on,- $off,- $once事件 API
- 过滤器(Filters)
- $children实例属性
- $destroy实例方法
2.3 性能对比
Vue 3 相比 Vue 2 在性能上有显著提升:
-  更小的包体积:Vue 3 核心库体积比 Vue 2 减小了约 41%,最小化和压缩后仅约 10KB。 
-  更快的初始渲染:Vue 3 初始渲染速度比 Vue 2 快约 55%。 
-  更高效的更新:由于优化的虚拟 DOM 和编译时提示,Vue 3 的更新性能比 Vue 2 快约 133%。 
-  内存占用更低:Vue 3 减少了约 54% 的内存使用量。 
2.4 生态系统适配
Vue 3 发布后,主要生态系统库逐步适配:
- Vue Router:4.x 版本支持 Vue 3
- Vuex:4.x 版本支持 Vue 3
- Pinia:新一代状态管理库,专为 Vue 3 设计
- Vite:新一代构建工具,原生支持 Vue 3
- Nuxt:3.x 版本支持 Vue 3
- UI 库:Element Plus、Vuetify 3、Quasar 2 等
2.5 迁移策略
从 Vue 2 迁移到 Vue 3 的建议策略:
- 渐进式迁移:使用 Vue 2.7(带有部分 Vue 3 特性)作为过渡。
- 使用迁移构建版本:Vue 3 提供了兼容 Vue 2 API 的构建版本。
- 使用迁移工具:Vue 团队提供了迁移助手工具。
- 分阶段迁移:先更新依赖,再更新代码风格,最后优化架构。
- 新项目直接使用 Vue 3:新项目建议直接采用 Vue 3。
3. 环境搭建与项目结构
3.1 安装 Vue
有多种方式可以在项目中使用 Vue:
3.1.1 直接引入
最简单的方法是通过 CDN 引入 Vue:
<!-- Vue 2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37/dist/vue.global.js"></script>
对于生产环境,应使用压缩版本:
<!-- Vue 2 生产版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
<!-- Vue 3 生产版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37"></script>
3.1.2 使用 npm
推荐使用 npm 管理依赖:
# Vue 2
npm install vue@2
# Vue 3
npm install vue@next
3.1.3 使用 Vue CLI
Vue CLI 是一个官方的项目脚手架工具:
# 安装 Vue CLI
npm install -g @vue/cli
# 创建一个新项目
vue create my-project
# 选择 Vue 2 或 Vue 3 作为默认预设
3.1.4 使用 Vite
Vite 是一个新一代的前端构建工具,由 Vue 团队开发:
# 使用 npm
npm init vite@latest my-vue-app -- --template vue
# 使用 yarn
yarn create vite my-vue-app --template vue
# 使用 pnpm
pnpm create vite my-vue-app -- --template vue
3.2 项目结构
典型的 Vue 项目结构(基于 Vue CLI 或 Vite 创建):
my-vue-project/
├── .vscode/                # VSCode 配置
├── node_modules/           # npm 依赖
├── public/                 # 静态资源,不经过 webpack 处理
│   ├── favicon.ico         # 网站图标
│   └── index.html          # HTML 模板
├── src/                    # 源代码
│   ├── assets/             # 资源文件(会被打包)
│   ├── components/         # 组件
│   ├── router/             # 路由配置(Vue Router)
│   ├── store/              # 状态管理(Vuex/Pinia)
│   ├── views/              # 视图/页面组件
│   ├── App.vue             # 根组件
│   └── main.js             # 入口文件
├── .browserslistrc         # 浏览器兼容性配置
├── .eslintrc.js            # ESLint 配置
├── .gitignore              # Git 忽略文件
├── babel.config.js         # Babel 配置
├── package.json            # 项目配置和依赖
├── README.md               # 项目说明文档
└── vue.config.js           # Vue CLI 配置文件
Vite 项目结构略有不同,通常没有 vue.config.js,而是 vite.config.js。
3.3 开发工具
有多种工具可帮助 Vue 开发:
3.3.1 Vue DevTools
Vue DevTools 是一个浏览器扩展,可以帮助调试 Vue 应用:
- 检查组件树
- 查看组件状态
- 跟踪事件
- 分析性能
- 时间旅行调试(Vuex/Pinia)
3.3.2 IDE 支持
Visual Studio Code 是最受欢迎的 Vue 开发 IDE,推荐以下扩展:
- Volar (Vue 3)
- Vetur (Vue 2)
- ESLint
- Prettier
- Vue VSCode Snippets
3.3.3 其他工具
- Vue CLI GUI:Vue CLI 的图形界面
- Vue Devtools Standalone:独立应用版 Vue Devtools
- Vite:快速的开发服务器和构建工具
- Nuxt DevTools:Nuxt.js 开发工具
3.4 配置文件
3.4.1 Vue CLI 配置
vue.config.js 文件可配置 Vue CLI 项目:
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/my-app/' : '/',
  outputDir: 'dist',
  assetsDir: 'static',
  productionSourceMap: false,
  devServer: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  },
  css: {
    loaderOptions: {
      sass: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
}
3.4.2 Vite 配置
vite.config.js 文件配置 Vite 项目:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false
  }
})
4. Vue 核心概念
4.1 声明式渲染
Vue.js 的核心是声明式渲染系统,允许我们声明式地将数据渲染为 DOM:
<div id="app">
  {
  { message }}
</div>
// Vue 2
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
// Vue 3
Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}).mount('#app')
声明式渲染的优势:
- 代码简洁易读
- 关注数据而非 DOM 操作
- 自动更新视图
- 可维护性更高
4.2 响应式系统
Vue 的响应式系统使得数据与视图保持同步:
4.2.1 Vue 2 响应式原理
Vue 2 使用 Object.defineProperty 实现响应式:
let data = { message: 'Hello' }
let vm = {}
Object.defineProperty(vm, 'message', {
  get() {
    return data.message
  },
  set(newValue) {
    data.message = newValue
    updateView() // 更新视图
  }
})
function updateView() {
  console.log('视图更新:', vm.message)
}
// 修改属性触发视图更新
vm.message = 'Hello Vue!'
Vue 2 响应式系统限制:
- 无法检测到对象属性的添加或删除
- 无法检测数组索引的变化和长度的变化
- 需要使用 Vue.set() 或 this.$set() 添加新属性
4.2.2 Vue 3 响应式原理
Vue 3 使用 Proxy 实现响应式:
let data = { message: 'Hello' }
const handler = {
  get(target, key) {
    track(target, key) // 依赖跟踪
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    const result = Reflect.set(target, key, value)
    trigger(target, key) // 触发更新
    return result
  }
}
const proxy = new Proxy(data, handler)
// 修改属性触发视图更新
proxy.message = 'Hello Vue 3!'
// 添加新属性也能触发视图更新
proxy.newProperty = 'New Value'
Vue 3 响应式系统优势:
- 可以检测对象属性的添加和删除
- 可以检测数组索引和长度的变化
- 可以监听 Map, Set, WeakMap, WeakSet
- 性能更好,消耗更少
4.3 指令系统
Vue 指令是带有 v- 前缀的特殊 HTML 属性,用于在模板中应用特殊的响应式行为:
4.3.1 常用内置指令
- v-bind: 动态绑定属性
- v-on: 绑定事件监听器
- v-if: 条件性渲染元素
- v-for: 基于数组渲染列表
- v-model: 表单输入绑定
- v-show: 切换元素的可见性
- v-slot: 插槽内容分发
- v-once: 一次性插值
- v-pre: 跳过编译
- v-cloak: 隐藏未编译的模板
- v-text: 设置文本内容
- v-html: 设置 HTML 内容
4.3.2 指令参数和修饰符
指令可以带参数和修饰符:
<!-- 参数 -->
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button>
<!-- 修饰符 -->
<form v-on:submit.prevent="onSubmit">表单</form>
<input v-model.trim="message">
4.3.3 自定义指令
可以注册自定义指令:
Vue 2:
// 全局注册
Vue.directive('focus', {
  inserted: function(el) {
    el.focus()
  }
})
// 局部注册
new Vue({
  directives: {
    focus: {
      inserted: function(el) {
        el.focus()
      }
    }
  }
})
Vue 3:
// 全局注册
const app = Vue.createApp({})
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})
// 局部注册
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  }
}
自定义指令钩子函数:
| Vue 2 | Vue 3 | 描述 | 
|---|---|---|
| bind | beforeMount | 指令绑定到元素时调用 | 
| inserted | mounted | 元素插入父节点时调用 | 
| update | - | 元素更新时调用(去除) | 
| componentUpdated | updated | 组件和子组件更新时调用 | 
| unbind | unmounted | 指令与元素解绑时调用 | 
4.4 生命周期
Vue 组件有一系列的生命周期钩子,允许在特定阶段执行代码:
4.4.1 Vue 2 生命周期
new Vue({
  beforeCreate() {
    // 实例初始化后,数据观测和事件配置之前
  },
  created() {
    // 实例创建完成后调用,此时数据已经可用
  },
  beforeMount() {
    // 挂载开始之前被调用,render 函数首次调用
  },
  mounted() {
    // 实例挂载到 DOM 后调用,可访问 DOM 元素
  },
  beforeUpdate() {
    // 数据更改导致虚拟 DOM 重新渲染前调用
  },
  updated() {
    // 虚拟 DOM 重新渲染后调用
  },
  activated() {
    // keep-alive 组件激活时调用
  },
  deactivated() {
    // keep-alive 组件停用时调用
  },
  beforeDestroy() {
    // 实例销毁前调用
  },
  destroyed() {
    // 实例销毁后调用
  },
  errorCaptured() {
    // 捕获子孙组件错误时调用
  }
})
4.4.2 Vue 3 生命周期
export default {
  // 选项式 API 生命周期
  beforeCreate() { /* ... */ },
  created() { /* ... */ },
  beforeMount() { /* ... */ },
  mounted() { /* ... */ },
  beforeUpdate() { /* ... */ },
  updated() { /* ... */ },
  beforeUnmount() { /* ... */ }, // 替代 beforeDestroy
  unmounted() { /* ... */ },     // 替代 destroyed
  activated() { /* ... */ },
  deactivated() { /* ... */ },
  errorCaptured() { /* ... */ },
  renderTracked() { /* ... */ },   // 新增:跟踪虚拟 DOM 重新渲染时调用
  renderTriggered() { /* ... */ }  // 新增:虚拟 DOM 重新渲染被触发时调用
}
4.4.3 Vue 3 组合式 API 生命周期钩子
import { 
  onBeforeMount, 
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered
} from 'vue'
export default {
  setup() {
    // 注意:没有 beforeCreate 和 created 对应的钩子
    // setup 本身在 beforeCreate 之后、created 之前执行
    
    onBeforeMount(() => { /* ... */ })
    onMounted(() => { /* ... */ })
    onBeforeUpdate(() => { /* ... */ })
    onUpdated(() => { /* ... */ })
    onBeforeUnmount(() => { /* ... */ })
    onUnmounted(() => { /* ... */ })
    onActivated(() => { /* ... */ })
    onDeactivated(() => { /* ... */ })
    onErrorCaptured(() => { /* ... */ })
    onRenderTracked(() => { /* ... */ })
    onRenderTriggered(() => { /* ... */ })
  }
}
4.5 Vue 实例属性和方法
Vue 实例提供了许多有用的属性和方法:
4.5.1 Vue 2 实例属性和方法
实例属性:
- $data: Vue 实例监视的数据对象
- $props: 当前组件接收的 props
- $el: Vue 实例使用的根 DOM 元素
- $options: 当前 Vue 实例的初始化选项
- $parent: 父实例
- $root: 根 Vue 实例
- $children: 当前实例的直接子组件
- $slots: 访问插槽内容
- $scopedSlots: 访问作用域插槽
- $refs: 持有注册过 ref 的所有 DOM 元素和组件实例
- $isServer: 当前 Vue 实例是否运行于服务端
- $attrs: 包含父作用域中非 prop 的属性绑定
- $listeners: 包含父作用域中的事件监听器
实例方法:
- $watch(): 观察 Vue 实例变化的一个表达式或计算属性函数
- $set(): 全局 Vue.set 的别名
- $delete(): 全局 Vue.delete 的别名
- $on(): 监听当前实例上的自定义事件
- $once(): 监听一个自定义事件,但只触发一次
- $off(): 移除自定义事件监听器
- $emit(): 触发当前实例上的事件
- $mount(): 手动挂载一个未挂载的实例
- $forceUpdate(): 强制 Vue 实例重新渲染
- $nextTick(): 将回调延迟到下次 DOM 更新循环之后执行
- $destroy(): 完全销毁一个实例
4.5.2 Vue 3 实例属性和方法
Vue 3 移除了部分实例属性和方法,如 $on, $off, $once, $children 等。其余大部分属性保持不变,但获取方式可能不同:
// 选项式 API 中访问实例属性
export default {
  mounted() {
    console.log(this.$data)
    console.log(this.$el)
  }
}
// 组合式 API 中访问实例属性
import { getCurrentInstance } from 'vue'
export default {
  setup() {
    const instance = getCurrentInstance()
    
    // 在 setup 中访问实例(仅在开发环境可用)
    console.log(instance.data)
    console.log(instance.proxy.$el) // 需要通过 proxy 访问
  }
}
5. Vue 实例详解
5.1 创建 Vue 实例
5.1.1 Vue 2 创建实例
在 Vue 2 中,通过 new Vue() 创建实例:
const vm = new Vue({
  // 选项
})
5.1.2 Vue 3 创建应用
在 Vue 3 中,通过 createApp() 创建应用实例:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
Vue 3 的设计更加模块化,全局 API 移至应用实例:
// Vue 2
Vue.component('my-component', { /* ... */ })
Vue.directive('my-directive', { /* ... */ })
Vue.mixin({ /* ... */ })
// Vue 3
const app = createApp(App)
app.component('my-component', { /* ... */ })
app.directive('my-directive', { /* ... */ })
app.mixin({ /* ... */ })
5.2 数据与方法
5.2.1 data 选项
Vue 实例的核心是 data 选项,它是一个对象或函数:
// Vue 2 根实例:对象语法
new Vue({
  data: {
    message: 'Hello'
  }
})
// Vue 2 组件:函数语法
Vue.component('my-component', {
  data() {
    return {
      message: 'Hello'
    }
  }
})
// Vue 3:始终使用函数语法
export default {
  data() {
    return {
      message: 'Hello'
    }
  }
}
为什么组件的 data 必须是函数? 为了确保每个组件实例有独立的数据副本,防止多个组件实例共享同一个数据对象。
5.2.2 methods 选项
methods 选项用于添加方法:
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 0
    }
  }
}
方法中的 this 自动绑定到 Vue 实例。
注意事项:
- 不要使用箭头函数定义 method,因为箭头函数没有自己的 this
- 方法可以在模板中直接调用,也可以用于事件处理
5.3 计算属性
计算属性是基于响应式依赖缓存的。只有相关依赖发生变化时才会重新计算:
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // 基本用法
    fullName() {
      return this.firstName + ' ' + this.lastName
    },
    
    // 带 getter 和 setter
    fullNameWithSetter: {
      get() {
        return this.firstName + ' ' + this.lastName
      },
      set(newValue) {
        const names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    }
  }
}
计算属性的优势:
- 缓存基于依赖,只有依赖变化时才重新计算
- 声明式编程,更简洁易读
- 自动跟踪依赖关系
5.4 侦听器
侦听器用于观察和响应 Vue 实例上的数据变动:
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // 基本用法
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    },
    
    // 深度侦听
    someObject: {
      handler(newValue, oldValue) {
        console.log('someObject changed')
      },
      deep: true
    },
    
    // 立即执行
    otherProperty: {
      handler(newValue, oldValue) {
        console.log('otherProperty changed')
      },
      immediate: true
    }
  },
  methods: {
    getAnswer() {
      this.answer = 'Thinking...'
      setTimeout(() => {
        this.answer = 'I think you should...'
      }, 1000)
    }
  }
}
侦听器特性:
- 可以执行异步操作
- 可以访问新值和旧值
- 可以深度侦听对象变化
- 可以在创建后立即执行
5.5 组件间通信
Vue 提供了多种组件通信方式:
5.5.1 Props 向下传递数据
父组件向子组件传递数据:
// 子组件
export default {
  props: {
    title: String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise
  }
}
// 父组件
<blog-post
  title="My journey with Vue"
  :likes="42"
  :is-published="true"
></blog-post>
5.5.2 自定义事件向上传递数据
子组件向父组件传递数据:
// 子组件
export default {
  methods: {
    incrementCounter() {
      this.$emit('increment', 1)
    }
  }
}
// 父组件
<button-counter @increment="incrementTotal"></button-counter>
5.5.3 其他通信方式
- refs:直接访问子组件
- provide/inject:祖先组件向所有子孙组件注入数据
- EventBus:Vue 2 中创建一个事件总线(不推荐)
- Vuex/Pinia:专门的状态管理解决方案
- mitt/tiny-emitter:Vue 3 中的事件库替代 EventBus
6. 模板语法与渲染
6.1 插值
6.1.1 文本插值
使用双大括号语法插入文本:
<span>Message: {
  { msg }}</span>
6.1.2 原始 HTML
使用 v-html 指令插入 HTML:
<div v-html="rawHtml"></div>
安全警告:使用 v-html 可能导致 XSS 攻击,只对可信内容使用,永不用于用户提供的内容。
6.1.3 属性绑定
使用 v-bind 指令绑定 HTML 属性:
<div v-bind:id="dynamicId"></div>
<!-- 简写 -->
<div :id="dynamicId"></div>
<!-- 布尔属性 -->
<button :disabled="isButtonDisabled">Button</button>
<!-- 多个属性绑定 -->
<div v-bind="{ id: 'container', class: 'wrapper' }"></div>
6.2 JavaScript 表达式
Vue 模板中支持完整的 JavaScript 表达式:
{
  { number + 1 }}
{
  { ok ? 'YES' : 'NO' }}
{
  { message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
限制:每个绑定只能包含单个表达式,不支持语句或控制流。
6.3 指令详解
指令是带有 v- 前缀的特殊 attribute,指令的值是单个 JavaScript 表达式。
6.3.1 参数
指令可以接收参数,在指令名之后以冒号表示:
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button>
6.3.2 动态参数
可以用方括号括起来的 JavaScript 表达式作为指令的参数:
<a v-bind:[attributeName]="url">链接</a>
<button v-on:[eventName]="doSomething">点击</button>
6.3.3 修饰符
修饰符是以点开头的特殊后缀,表示指令应该以特殊方式绑定:
<!-- 阻止默认行为 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 键盘事件 -->
<input v-on:keyup.enter="submit">
<!-- 表单修饰符 -->
<input v-model.trim="msg">
6.4 缩写
Vue 为最常用的指令提供了缩写:
<!-- 完整语法 -->
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button>
<!-- 缩写 -->
<a :href="url">链接</a>
<button @click="doSomething">点击</button>
<!-- 动态参数缩写 -->
<a :[key]="url">链接</a>
<button @[event]="doSomething">点击</button>
6.5 模板中的条件与循环
6.5.1 条件渲染
使用 v-if, v-else-if, v-else 进行条件渲染:
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
使用 v-show 控制元素显示:
<h1 v-show="ok">Hello!</h1>
v-if 与 v-show 对比:
- v-if是"真正"的条件渲染,元素会被销毁和重建
- v-show只是切换 CSS- display属性
- v-if有更高的切换开销,- v-show有更高的初始渲染开销
- 频繁切换使用 v-show,条件很少改变使用v-if
6.5.2 列表渲染
使用 v-for 基于数组渲染列表:
<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {
  { index }} - {
  { item.text }}
  </li>
</ul>
v-for 也可以遍历对象属性:
<ul>
  <li v-for="(value, key, index) in object" :key="key">
    {
  { index }}. {
  { key }}: {
  { value }}
  </li>
</ul>
v-for 与 v-if 不应在同一元素上使用,因为 v-for 比 v-if 优先级更高。
6.6 过滤器 (Vue 2)
Vue 2 支持过滤器,可用于文本格式化:
<!-- 在双花括号中 -->
{
  { message | capitalize }}
<!-- 在 v-bind 中 -->
<div v-bind:id="rawId | formatId"></div>
filters: {
  capitalize(value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}
注意:Vue 3 已移除过滤器,推荐使用方法或计算属性代替。
7. 计算属性与侦听器
7.1 计算属性详解
计算属性是基于其响应式依赖进行缓存的。
7.1.1 基本用法
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  computed: {
    // 计算属性的 getter
    reversedMessage() {
      // `this` 指向组件实例
      return this.message.split('').reverse().join('')
    }
  }
}
7.1.2 计算属性缓存 vs 方法
计算属性和方法的区别:
- 计算属性基于依赖缓存,依赖不变时不会重新计算
- 方法在每次重新渲染时都会执行
- 对于计算开销大的操作,应优先使用计算属性
// 计算属性(有缓存)
computed: {
  expensiveOperation() {
    console.log('Computing expensive operation')
    return this.items.filter(item => item.important).map(...)
  }
}
// 方法(无缓存)
methods: {
  expensiveMethod() {
    console.log('Running expensive method')
    return this.items.filter(item => item.important).map(...)
  }
}
7.1.3 计算属性的 setter
计算属性默认只有 getter,但也可以提供 setter:
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        const names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1] || ''
      }
    }
  }
}
使用 setter:
// 此时会调用 setter: this.firstName 和 this.lastName 会相应更新
this.fullName = 'Jane Smith'
7.2 侦听器详解
侦听器适用于需要在数据变化时执行异步或开销较大的操作。
7.2.1 基本用法
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // 每当 question 发生变化时,该函数将会执行
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    getAnswer() {
      this.answer = 'Thinking...'
      axios
        .get('https://api.example.com/answer')
        .then(response => {
          this.answer = response.data.answer
        })
        .catch(error => {
          this.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
}
7.2.2 深度侦听
默认情况下,侦听器只会监听顶层属性的变化。使用 deep 选项进行深度侦听:
export default {
  data() {
    return {
      user: {
        name: 'John',
        profile: {
          age: 30,
          address: {
            city: 'New York'
          }
        }
      }
    }
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        console.log('User object changed')
      },
      deep: true
    },
    // 也可以直接监听嵌套属性
    'user.profile.age'(newValue, oldValue) {
      console.log('Age changed from', oldValue, 'to', newValue)
    }
  }
}
7.2.3 立即执行
使用 immediate 选项可以使侦听器在创建时立即执行:
watch: {
  question: {
    handler(newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    },
    immediate: true
  }
}
7.2.4 停止侦听
在选项式 API 中,侦听器会在组件销毁时自动停止。如果需要提前停止,可以使用 $watch 返回的取消函数:
const unwatch = this.$watch('question', (newQuestion) => {
  // ...
})
// 当不再需要观察时,调用取消函数
unwatch()
在组合式 API 中,watch 函数返回停止侦听的函数:
import { ref, watch } from 'vue'
export default {
  setup() {
    const question = ref('')
    
    // 开始侦听
    const stopWatching = watch(question, (newQuestion) => {
      // ...
    })
    
    // 停止侦听
    stopWatching()
  }
}
7.3 计算属性 vs 侦听器
计算属性和侦听器的选择原则:
-  计算属性:适用于根据现有数据派生出新数据 - 更加声明式
- 有缓存机制
- 适合同步计算
 
-  侦听器:适用于响应数据变化 - 支持异步操作
- 可以执行副作用(如修改 DOM、发送网络请求)
- 可以访问变化前后的值
- 可以监视多个数据源
 
7.3.1 何时使用计算属性
// 使用计算属性(更好)
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}
// 使用侦听器(不推荐)
watch: {
  firstName(newVal) {
    this.fullName = newVal + ' ' + this.lastName
  },
  lastName(newVal) {
    this.fullName = this.firstName + ' ' + newVal
  }
}
7.3.2 何时使用侦听器
// 使用侦听器
watch: {
  searchQuery(newQuery) {
    this.isLoading = true
    this.debouncedGetResults(newQuery)
  }
},
methods: {
  // 防抖函数
  debounce(fn, delay) {
    let timeout
    return function(...args) {
      clearTimeout(timeout)
      timeout = setTimeout(() => fn.apply(this, args), delay)
    }
  },
  created() {
    this.debouncedGetResults = this.debounce(this.getResults, 300)
  },
  getResults(query) {
    axios.get('/api/search', { params: { query } })
      .then(response => {
        this.results = response.data
        this.isLoading = false
      })
  }
}
8. Class 与 Style 绑定
8.1 绑定 HTML Class
8.1.1 对象语法
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
data() {
  return {
    isActive: true,
    hasError: false
  }
}
渲染结果:
<div class="active"></div>
也可以绑定一个对象:
<div :class="classObject"></div>
data() {
  return {
    classObject: {
      active: true,
      'text-danger': false
    }
  }
}
或使用计算属性:
computed: {
  classObject() {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}
8.1.2 数组语法
<div :class="[activeClass, errorClass]"></div>
data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}
渲染结果:
<div class="active text-danger"></div>
数组中使用条件:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
更简洁的方式是在数组中使用对象语法:
<div :class="[{ active: isActive }, errorClass]"></div>
8.1.3 与组件结合使用
当在自定义组件上使用 class 时,这些类将被添加到组件的根元素上:
<!-- 假设这是父组件模板 -->
<my-component class="baz"></my-component>
// 组件定义
Vue.component('my-component', {
  template: '<p class="foo bar">Hi</p>'
})
渲染结果:
<p class="foo bar baz">Hi</p>
8.2 绑定内联样式
8.2.1 对象语法
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() {
  return {
    activeColor: 'red',
    fontSize: 30
  }
}
通常绑定一个样式对象会更清晰:
<div :style="styleObject"></div>
data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}
8.2.2 数组语法
可以将多个样式对象应用到同一个元素上:
<div :style="[baseStyles, overridingStyles]"></div>
data() {
  return {
    baseStyles: {
      color: 'blue',
      fontSize: '16px'
    },
    overridingStyles: {
      fontWeight: 'bold',
      border: '1px solid black'
    }
  }
}
8.2.3 自动前缀
使用 :style 时,Vue 会自动为需要添加浏览器前缀的 CSS 属性添加适当的前缀:
<div :style="{ display: 'flex' }"></div>
渲染结果(取决于浏览器):
<div style="display: -webkit-flex; display: flex;"></div>
8.2.4 多重值
从 Vue 2.3 开始,可以为样式属性提供一个包含多个值的数组,只会渲染数组中浏览器支持的最后一个值:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
渲染结果(在支持 flex 的浏览器中):
<div style="display: flex;"></div>
9. 条件渲染
9.1 v-if 指令
v-if 指令用于条件性地渲染一块内容,只有当表达式为 truthy 时,内容才会被渲染:
<h1 v-if="awesome">Vue is awesome!</h1>
9.2 v-else 和 v-else-if
v-else 和 v-else-if 必须紧跟在 v-if 或 v-else-if 元素之后:
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
9.3 在 <template> 上使用 v-if
 
对于需要同时条件渲染多个元素,可以使用不可见的 <template> 元素包裹,最终渲染结果将不包含 <template> 元素:
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>
9.4 v-show 指令
v-show 也用于条件性显示元素,但元素始终会被渲染并保留在 DOM 中,只是简单地切换 CSS 的 display 属性:
<h1 v-show="ok">Hello!</h1>
注意:v-show 不支持在 <template> 元素上使用,也不支持 v-else。
9.5 v-if vs v-show
-  v-if: - "真正"的条件渲染(创建和销毁元素)
- 有更高的切换开销
- 在运行时条件很少改变时使用
- 支持 <template>,v-else和v-else-if
 
-  v-show: - 元素始终被渲染,只是切换 CSS display属性
- 有更高的初始渲染开销
- 需要频繁切换时使用
- 不支持 <template>和v-else
 
- 元素始终被渲染,只是切换 CSS 
9.6 v-if 与 v-for 一起使用
不推荐同时使用 v-if 和 v-for:当它们同时存在于一个元素上时,v-for 的优先级高于 v-if。
下面的代码,v-if 将分别重复运行于每个 v-for 循环中:
<!-- 不推荐 -->
<ul>
  <li v-for="user in users" v-if="user.active" :key="user.id">
    {
  { user.name }}
  </li>
</ul>
两个建议的替代方案:
- 使用计算属性过滤:
<!-- 推荐 -->
<ul>
  <li v-for="user in activeUsers" :key="user.id">
    {
  { user.name }}
  </li>
</ul>
computed: {
  activeUsers() {
    return this.users.filter(user => user.active)
  }
}
- 使用 <template>和v-for把v-if移到外层:
<!-- 推荐 -->
<ul>
  <template v-for="user in users" :key="user.id">
    <li v-if="user.active">
      {
  { user.name }}
    </li>
  </template>
</ul>
10. 列表渲染
10.1 v-for 基础用法
10.1.1 遍历数组
使用 v-for 指令基于一个数组来渲染一个列表:
<ul>
  <li v-for="item in items" :key="item.id">
    {
  { item.text }}
  </li>
</ul>
也可以访问当前项的索引:
<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {
  { index }} - {
  { item.text }}
  </li>
</ul>
10.1.2 遍历对象
可以用 v-for 遍历对象的属性:
<ul>
  <li v-for="value in object" :key="value">
    {
  { value }}
  </li>
</ul>
可以提供第二个参数为键名:
<ul>
  <li v-for="(value, key) in object" :key="key">
    {
  { key }}: {
  { value }}
  </li>
</ul>
还可以提供第三个参数为索引:
<ul>
  <li v-for="(value, key, index) in object" :key="key">
    {
  { index }}. {
  { key }}: {
  { value }}
  </li>
</ul>
10.1.3 遍历数字范围
v-for 也可以接受整数,会重复对应次数:
<div>
  <span v-for="n in 10" :key="n">{
  { n }} </span>
</div>
注意这里的 n 是从 1 开始,而不是从 0 开始。
10.2 维护状态与 key
Vue 默认使用"就地更新"的策略,如果数据项的顺序被改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是更新每个元素。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一的 key 属性:
<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>
key 的最佳实践:
- 使用唯一标识符(如 id)作为 key
- 不要使用索引作为 key(除非列表是静态的且不会重新排序)
- 尽量不要使用随机值作为 key,这会导致性能低下
10.3 数组更新检测
10.3.1 变更方法
Vue 能够检测响应式数组的变更方法,这些方法会触发视图更新:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
示例:
// 这些方法会触发视图更新
this.items.push({ id: 4, text: 'New Item' })
this.items.pop()
10.3.2 替换数组
有些方法不会改变原始数组,而是返回一个新数组,例如:
- filter()
- concat()
- slice()
当使用这些方法时,可以用新数组替换旧数组:
// 替换整个数组(Vue 能够检测)
this.items = this.items.filter(item => item.text.match(/Foo/))
10.3.3 注意事项
由于 JavaScript 的限制,Vue 不能检测到数组的以下变动:
-  利用索引直接设置一个数组项: // 这不会触发视图更新(Vue 2) this.items[index] = newValue
-  修改数组的长度: // 这不会触发视图更新(Vue 2) this.items.length = newLength
解决方案(Vue 2):
// 使用 Vue.set
Vue.set(this.items, index, newValue)
// 或
this.$set(this.items, index, newValue)
// 使用 splice
this.items.splice(index, 1, newValue)
// 修改长度
this.items.splice(newLength)
在 Vue 3 中,使用 Proxy 解决了这些问题,直接修改索引或长度会触发更新。
10.4 显示过滤/排序后的结果
显示一个数组的过滤或排序副本,而不实际改变原始数据:
<ul>
  <li v-for="n in evenNumbers" :key="n">{
  { n }}</li>
</ul>
data() {
  return {
    numbers: [1, 2, 3, 4, 5]
  }
},
computed: {
  evenNumbers() {
    return this.numbers.filter(n => n % 2 === 0)
  }
}
对于复杂的情况,也可以使用方法:
<ul>
  <li v-for="n in even(numbers)" :key="n">{
  { n }}</li>
</ul>
data() {
  return {
    numbers: [1, 2, 3, 4, 5]
  }
},
methods: {
  even(numbe