重新学习前端之Vue

Vue

一、Vue 基础概念

1. 什么是 Vue.js?

核心特性

  1. 数据驱动视图:采用 MVVM 模式,数据变化自动更新视图
  2. 组件化开发:将页面拆分为独立可复用的组件
  3. 双向数据绑定:通过 v-model 实现表单数据的双向同步
  4. 虚拟 DOM:提升 DOM 操作性能
  5. 响应式系统:数据变化自动追踪并更新依赖
  6. 单文件组件 (SFC):.vue 文件将模板、逻辑、样式封装在一起

Vue 与传统开发的区别

对比维度 jQuery/传统开发 Vue.js
操作对象 DOM 数据
更新方式 手动操作 DOM 自动更新
开发模式 命令式 声明式
数据绑定 手动同步 双向绑定
代码组织 全局/散乱 组件化

渐进式框架的含义

Vue 可以自底向上逐层应用:

  • 核心库只关注视图层,易于上手
  • 可以只在一个页面的一小部分使用 Vue
  • 可以配合官方生态(Router、Vuex、CLI)构建复杂单页应用
  • 可以与第三方库共存

2. MVVM 模式及其在 Vue 中的体现

MVVM 定义

MVVM(Model-View-ViewModel)是一种软件架构模式,是 MVC 的改进版:

  • Model(模型):数据模型,负责业务逻辑和数据存储
  • View(视图):UI 层,负责展示数据
  • ViewModel(视图模型):连接 Model 和 View 的桥梁,负责数据双向绑定

MVVM 在 Vue 中的体现

scss 复制代码
┌─────────────────────────────────────────────┐
│                  MVVM 架构                    │
├─────────────────────────────────────────────┤
│                                             │
│  View                    ViewModel          │
│  (DOM 模板)      ←→     (Vue 实例)          │
│                        /         \          │
│                       /           \         │
│              数据绑定             事件监听    │
│                     /             \         │
│                    /               \        │
│              Model (data/响应式数据)         │
│                                             │
└─────────────────────────────────────────────┘

具体体现

MVVM 元素 Vue 对应实现
Model Vue 实例中的 datacomputed 等响应式数据
View 模板语法 {{}}、指令渲染的 DOM
ViewModel Vue 实例本身(new Vue({...})

数据流向

javascript 复制代码
// 数据变化 → 视图更新(自动)
this.message = 'Hello Vue'  // DOM 自动更新显示

// 视图变化 → 数据更新(双向绑定)
<input v-model="message">   // 输入框变化自动更新 message

3. Vue 的优点与缺点

优点

  • 简单易学,API 设计友好
  • 轻量级,核心库体积小(gzip 后约 20KB)
  • 双向数据绑定,减少手动 DOM 操作
  • 组件化开发,提高代码复用性
  • 官方生态完善(Vue Router、Vuex、Vue CLI)
  • 虚拟 DOM 提升性能
  • 支持过渡动画、自定义指令等高级特性
  • 文档友好,中文文档完善

缺点

  • 生态圈相对 React 较小
  • 不适合超大型项目(组件层级过深时性能下降)
  • 首屏加载较慢(SPA 通病)
  • SEO 不友好(需配合 SSR 解决)
  • IE 兼容性有限(Vue 3 不支持 IE)

4. Vue 与 React 的对比

对比维度 Vue React
核心思想 MVVM 模式 函数式 UI
模板语法 基于 HTML 的模板语法 JSX(JavaScript XML)
数据绑定 双向绑定 单向数据流
状态管理 Vuex Redux/MobX
响应式 Object.defineProperty/Proxy setState/hooks
学习曲线 平缓,易于上手 较陡,需理解 JSX、Hooks 等
性能优化 自动依赖追踪 手动 shouldComponentUpdate/memo
类型支持 TypeScript 支持(Vue 3 更好) TypeScript 原生支持好

选择策略

  • 选 Vue:中小型项目、快速开发、团队对 Vue 熟悉、需要双向绑定
  • 选 React:大型复杂项目、需要灵活架构、生态要求高、团队有 React 经验

5. Vue 与 jQuery 的区别

对比维度 jQuery Vue
操作对象 DOM 数据
编程模式 命令式(手动操作) 声明式(描述状态)
数据更新 手动 $('#el').text() 自动响应式更新
代码组织 散乱 组件化
适用场景 简单交互、老项目维护 现代 SPA 应用
javascript 复制代码
// jQuery 方式:手动操作 DOM
$('#btn').click(function() {
  var count = parseInt($('#count').text());
  $('#count').text(count + 1);
});

// Vue 方式:数据驱动
<button @click="count++">{{ count }}</button>

二、Vue 实例与生命周期

1. Vue 生命周期概述

定义

每个 Vue 实例在被创建时都要经过一系列的初始化过程,这个过程就是生命周期。生命周期钩子是实例在特定阶段自动执行的函数。

生命周期的 8 个阶段

markdown 复制代码
        beforeCreate → created → beforeMount → mounted
              ↓            ↓           ↓           ↓
           实例初始化   数据注入完成   模板编译完成   DOM 渲染完成
              ↓            ↓           ↓           ↓
        beforeUpdate → updated → beforeDestroy → destroyed
              ↓            ↓           ↓           ↓
           数据变化前   数据变化后   实例销毁前   实例销毁后

各阶段详细说明

生命周期钩子 触发时机 可访问内容 常见用途
beforeCreate 实例初始化后,数据观测和事件配置之前 无(data/props/methods 都未初始化) 极少使用
created 实例创建完成,数据观测、属性计算完成 data、methods、computed、watch 发起 API 请求、初始化数据
beforeMount 模板编译完成,挂载开始前 可以访问模板,但 DOM 还未生成 挂载前最后修改数据的机会
mounted 实例挂载到 DOM 后 可以访问真实 DOM DOM 操作、第三方库初始化
beforeUpdate 数据变化导致虚拟 DOM 重新渲染前 可以访问更新前后的数据 更新前清理或记录状态
updated 虚拟 DOM 重新渲染和打补丁后 可以访问更新后的 DOM DOM 更新后的操作
beforeDestroy 实例销毁前 实例仍然完全可用 清理定时器、解绑事件
destroyed 实例销毁后 所有东西都被解绑 最终清理工作

代码示例

javascript 复制代码
export default {
  data() {
    return {
      message: 'Hello Vue'
    }
  },
  beforeCreate() {
    console.log('beforeCreate: 实例初始化')
    // console.log(this.message) // undefined
  },
  created() {
    console.log('created: 实例创建完成')
    console.log(this.message) // 'Hello Vue'
    // 发起 API 请求的最佳时机
    this.fetchData()
  },
  beforeMount() {
    console.log('beforeMount: 挂载前')
    // DOM 还未生成
  },
  mounted() {
    console.log('mounted: 已挂载')
    // 可以操作 DOM
    console.log(this.$el)
  },
  beforeUpdate() {
    console.log('beforeUpdate: 更新前')
  },
  updated() {
    console.log('updated: 已更新')
  },
  beforeDestroy() {
    console.log('beforeDestroy: 销毁前')
    // 清理定时器、事件监听器
    clearInterval(this.timer)
  },
  destroyed() {
    console.log('destroyed: 已销毁')
  },
  methods: {
    fetchData() {
      // 模拟 API 请求
      setTimeout(() => {
        this.message = 'Data loaded'
      }, 1000)
    }
  }
}

常见误区

  1. ❌ 在 beforeCreate 中访问 data → data 还未初始化
  2. ❌ 在 created 中操作 DOM → DOM 还未生成
  3. ❌ 在 mounted 中发起大量请求 → 可能导致页面卡顿,应考虑在 created 中发起
  4. ❌ 忘记在 beforeDestroy 中清理资源 → 导致内存泄漏

2. created 与 mounted 的区别

对比维度 created mounted
触发时机 实例创建完成后 DOM 挂载完成后
能否访问 data ✅ 可以 ✅ 可以
能否访问 DOM ❌ 不能(DOM 未生成) ✅ 可以
能否访问 $el ❌ 不能 ✅ 可以
服务端渲染 SSR ✅ 会执行 ❌ 不执行
适用场景 数据初始化、API 请求 DOM 操作、图表初始化、第三方库

选择策略

  • 如果只是获取数据,优先使用 created(SSR 友好,更早执行)
  • 如果需要操作 DOM,必须使用 mounted
javascript 复制代码
export default {
  created() {
    // ✅ 适合:数据请求
    this.getUserInfo()
    this.getArticleList()
  },
  mounted() {
    // ✅ 适合:DOM 操作
    this.initChart()
    this.focusInput()
    
    // ✅ 适合:第三方库初始化
    this.editor = new Editor(this.$refs.editorEl)
  }
}

3. 父子组件生命周期执行顺序

加载渲染过程

复制代码
父 beforeCreate
父 created
父 beforeMount
  ↓ 递归渲染子组件
子 beforeCreate
子 created
子 beforeMount
子 mounted
  ↓ 子组件挂载完成
父 mounted

更新过程

复制代码
父 beforeUpdate
  ↓ 递归更新子组件
子 beforeUpdate
子 updated
  ↓ 子组件更新完成
父 updated

销毁过程

复制代码
父 beforeDestroy
  ↓ 递归销毁子组件
子 beforeDestroy
子 destroyed
  ↓ 子组件销毁完成
父 destroyed

代码验证

javascript 复制代码
// 父组件 Parent.vue
export default {
  name: 'Parent',
  beforeCreate() { console.log('Parent beforeCreate') },
  created() { console.log('Parent created') },
  beforeMount() { console.log('Parent beforeMount') },
  mounted() { console.log('Parent mounted') },
}

// 子组件 Child.vue
export default {
  name: 'Child',
  beforeCreate() { console.log('Child beforeCreate') },
  created() { console.log('Child created') },
  beforeMount() { console.log('Child beforeMount') },
  mounted() { console.log('Child mounted') },
}

// 输出顺序:
// Parent beforeCreate
// Parent created
// Parent beforeMount
// Child beforeCreate
// Child created
// Child beforeMount
// Child mounted
// Parent mounted

4. keep-alive 带来的生命周期变化

当组件被 <keep-alive> 包裹时:

  • activated:组件被激活时调用
  • deactivated:组件被停用时调用
  • beforeDestroydestroyed 不再触发(组件被缓存而非销毁)
html 复制代码
<template>
  <keep-alive>
    <router-view />
  </keep-alive>
</template>

<script>
export default {
  activated() {
    console.log('组件被激活')
    // 恢复状态、重新获取数据
  },
  deactivated() {
    console.log('组件被停用')
    // 保存状态、暂停定时器等
  }
}
</script>

5. Vue 实例挂载过程

挂载流程

markdown 复制代码
1. new Vue(options)
   ↓
2. 初始化生命周期、事件、数据
   ↓
3. 调用 $mount()
   ↓
4. 编译模板(compile)
   ↓
5. 生成渲染函数(render function)
   ↓
6. 创建虚拟 DOM
   ↓
7. 将虚拟 DOM 转换为真实 DOM
   ↓
8. 替换 el 指定的元素

源码流程简述

javascript 复制代码
// 简化版挂载流程
Vue.prototype.$mount = function(el) {
  el = el && document.querySelector(el)
  
  // 如果没有 render 函数,则编译模板
  if (!this.$options.render) {
    const template = this.$options.template
    if (template) {
      // 模板编译为渲染函数
      this.$options.render = compileToFunctions(template)
    }
  }
  
  // 调用挂载核心方法
  return mountComponent(this, el)
}

function mountComponent(vm, el) {
  vm.$el = el
  
  // 调用 beforeMount 钩子
  callHook(vm, 'beforeMount')
  
  // 定义更新组件
  const updateComponent = () => {
    vm._update(vm._render())
  }
  
  // 创建 Watcher
  new Watcher(vm, updateComponent, noop, { before() {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }})
  
  vm._isMounted = true
  callHook(vm, 'mounted')
  return vm
}

三、模板语法与指令系统

1. Vue 指令概述

定义

指令(Directives)是带有 v- 前缀的特殊 attribute,用于在响应式数据变化时,将副作用反应式地应用到 DOM 上。

2. 常用内置指令

指令 作用 示例
v-bind / : 动态绑定属性 <img :src="imgUrl">
v-on / @ 事件监听 <button @click="handleClick">
v-model 双向数据绑定 <input v-model="msg">
v-if 条件渲染(销毁/创建) <div v-if="show">
v-else 条件渲染的 else 分支 <div v-else>
v-else-if 条件渲染的 else-if 分支 <div v-else-if="type === 'A'">
v-show 条件显示(CSS 切换) <div v-show="visible">
v-for 列表渲染 <li v-for="item in list">
v-text 设置文本内容 <span v-text="msg">
v-html 设置 HTML 内容 <div v-html="htmlStr">
v-pre 跳过编译 <span v-pre>{{ raw }}</span>
v-once 只渲染一次 <span v-once>{{ static }}</span>
v-cloak 隐藏未编译模板 <div v-cloak>{{ msg }}</div>
v-slot / # 具名插槽 <template #header>

3. v-if 与 v-show 的区别

定义对比

对比维度 v-if v-show
实现方式 条件为 false 时,元素从 DOM 中移除 条件为 false 时,设置 display: none
初始渲染 惰性渲染(条件为 false 时不渲染) 始终渲染
切换开销 较大(创建/销毁 DOM) 较小(仅切换 CSS)
初始开销 较小 较大
适用场景 切换频率低 频繁切换

代码示例

html 复制代码
<template>
  <div>
    <!-- v-if:条件为 false 时,DOM 中不存在该元素 -->
    <div v-if="isVisible">v-if 控制的元素</div>
    
    <!-- v-show:条件为 false 时,DOM 中存在但 display: none -->
    <div v-show="isVisible">v-show 控制的元素</div>
  </div>
</template>

选择策略

  • 需要频繁切换 → 使用 v-show
  • 运行时条件不太可能改变 → 使用 v-if
  • 需要配合 <template> 使用 → 只能用 v-if
  • 需要配合 v-else / v-else-if → 只能用 v-if

常见误区

  1. ❌ v-show 支持 <template> 语法 → 实际上不支持
  2. ❌ v-if 性能一定差 → 初始不渲染时反而性能更好
  3. ❌ 两者可以混用同一元素 → 不推荐,v-if 优先级更高

4. v-for 与 v-if 的优先级

Vue 2 中v-for 优先级高于 v-if

html 复制代码
<!-- v-for 先执行,再对每个项目判断 v-if -->
<li v-for="item in list" v-if="item.active">
  {{ item.name }}
</li>

Vue 3 中v-if 优先级高于 v-for

最佳实践 :不要在同一元素上同时使用 v-forv-if

html 复制代码
<!-- ✅ 推荐:用 computed 过滤 -->
<li v-for="item in activeItems" :key="item.id">
  {{ item.name }}
</li>

<script>
export default {
  computed: {
    activeItems() {
      return this.list.filter(item => item.active)
    }
  }
}
</script>

<!-- ✅ 推荐:用 template 包裹 v-if -->
<template v-if="shouldShow">
  <li v-for="item in list" :key="item.id">
    {{ item.name }}
  </li>
</template>

5. v-model 的原理

定义
v-model 是 Vue 提供的语法糖,用于在表单元素上创建双向数据绑定。

原理
v-model 本质上是 :value@input 的组合:

html 复制代码
<!-- 等价写法 -->
<input v-model="message">

<!-- 等同于 -->
<input :value="message" @input="message = $event.target.value">

在不同表单元素上的实现

表单元素 绑定属性 触发事件
<input type="text"> value input
<textarea> value input
<select> value change
<input type="checkbox"> checked change
<input type="radio"> checked change

自定义组件上的 v-model

javascript 复制代码
// 父组件
<custom-input v-model="searchText"></custom-input>

// 子组件 CustomInput.vue
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input 
      :value="modelValue" 
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
}

v-model 修饰符

修饰符 作用 示例
.lazy 将 input 事件改为 change 事件 <input v-model.lazy="msg">
.number 自动将输入转为数字 <input v-model.number="age">
.trim 自动去除首尾空格 <input v-model.trim="name">

6. v-bind 与 v-model 的区别

对比维度 v-bind v-model
数据流向 单向绑定(父→子 / 数据→视图) 双向绑定
用途 绑定任意 HTML 属性 仅用于表单元素的双向绑定
实现 element.attribute = value :value + @input
示例 <img :src="url"> <input v-model="text">

7. v-on 修饰符

事件修饰符

修饰符 作用 等价原生代码
.stop 阻止事件冒泡 event.stopPropagation()
.prevent 阻止默认行为 event.preventDefault()
.capture 使用事件捕获模式 -
.self 只在事件目标是当前元素时触发 if (event.target === event.currentTarget)
.once 事件只触发一次 -
.passive 不阻止默认行为(提升滚动性能) -
html 复制代码
<!-- 阻止单击事件冒泡 -->
<a @click.stop="handleClick"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="handleClick"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 点击事件只会触发一次 -->
<a @click.once="handleClick"></a>

按键修饰符

html 复制代码
<!-- 只有在 key 是 Enter 时调用 -->
<input @keyup.enter="submit">

<!-- 常用按键别名 -->
.enter
.tab
.delete (捕获"删除"和"退格"键)
.esc
.space
.up
.down
.left
.right

<!-- 系统修饰键 -->
.ctrl
.alt
.shift
.meta (Mac 上是 ⌘)

<!-- 精确匹配 -->
<input @keyup.exact="handleKeyup">
<input @keyup.ctrl.exact="handleCtrlKeyup">

8. v-slot 的用法

基本用法

html 复制代码
<!-- 父组件 -->
<base-layout>
  <template v-slot:header>
    <h1>这里是头部</h1>
  </template>
  
  <template #default>
    <p>这里是默认内容</p>
  </template>
  
  <template #footer>
    <p>这里是底部</p>
  </template>
</base-layout>

<!-- 子组件 BaseLayout.vue -->
<template>
  <div class="layout">
    <header><slot name="header"></slot></header>
    <main><slot></slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

9. v-pre / v-cloak / v-once

v-pre:跳过该元素及其子元素的编译,用于显示原始 Mustache 标签

html 复制代码
<span v-pre>{{ 这里的内容不会被编译 }}</span>
<!-- 渲染结果:<span>{{ 这里的内容不会被编译 }}</span> -->

v-cloak:保持在元素上直到关联实例结束编译,配合 CSS 隐藏未编译的 Mustache 标签

css 复制代码
[v-cloak] {
  display: none;
}
html 复制代码
<div v-cloak>
  {{ message }}
</div>

v-once:只渲染元素和组件一次,后续更新跳过

html 复制代码
<span v-once>{{ message }}</span>
<!-- 后续 message 变化时,这里不会更新 -->

10. 自定义指令

全局注册

javascript 复制代码
// main.js
Vue.directive('focus', {
  bind(el) {
    // 指令绑定到元素时调用(只调用一次)
  },
  inserted(el) {
    // 元素插入父节点时调用
    el.focus()
  },
  update(el, binding) {
    // 组件更新前调用
  },
  componentUpdated(el, binding) {
    // 组件及其子组件更新后调用
  },
  unbind(el) {
    // 指令解绑时调用(只调用一次)
  }
})

局部注册

javascript 复制代码
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus()
      }
    }
  }
}

使用自定义指令

html 复制代码
<input v-focus>

钩子函数参数

  • el:指令绑定的元素
  • binding:包含 namevalueoldValueexpressionargmodifiers
  • vnode:虚拟节点
  • oldVnode:上一个虚拟节点

四、计算属性与侦听器

1. 计算属性(computed)

定义

计算属性是基于响应式依赖进行缓存的派生状态。只有依赖的响应式数据发生变化时,才会重新计算。

基本用法

javascript 复制代码
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // 只读计算属性
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}

getter 和 setter

javascript 复制代码
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      get() {
        return `${this.firstName} ${this.lastName}`
      },
      set(newValue) {
        const names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    }
  }
}

2. computed 与 methods 的区别

对比维度 computed methods
缓存 ✅ 有缓存(依赖不变不重新计算) ❌ 每次调用都执行
调用方式 像属性一样使用 \{\{ fullName \}\} 像函数一样调用 \{\{ getName() \}\}
适用场景 依赖响应式数据的计算结果 需要传参或无依赖的计算
性能 更高(缓存机制) 较低(每次执行)
html 复制代码
<template>
  <div>
    <!-- computed:不重新执行,直接返回缓存 -->
    <p>{{ reversedMessage }}</p>
    
    <!-- methods:每次渲染都会执行 -->
    <p>{{ reverseMessage() }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { message: 'Hello' }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  },
  methods: {
    reverseMessage() {
      return this.message.split('').reverse().join('')
    }
  }
}
</script>

3. computed 与 watch 的区别

对比维度 computed watch
用途 派生状态(一个依赖多个 → 输出一个) 响应数据变化(执行副作用)
缓存 ✅ 有缓存 ❌ 无缓存
异步支持 ❌ 不支持异步 ✅ 支持异步
返回值 ✅ 必须 return ❌ 不需要 return
性能 更高 较低
适用场景 数据转换、过滤、计算 异步操作、开销大的操作
javascript 复制代码
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe',
      fullName: ''
    }
  },
  computed: {
    // 计算属性:自动缓存
    computedFullName() {
      return `${this.firstName} ${this.lastName}`
    }
  },
  watch: {
    // 侦听器:支持异步
    firstName(newVal) {
      this.fullName = `${newVal} ${this.lastName}`
    },
    // 深度监听
    obj: {
      handler(newVal) {
        console.log('对象变化', newVal)
      },
      deep: true,
      immediate: true
    }
  }
}

4. watch 的用法

基本侦听

javascript 复制代码
watch: {
  // 监听基础类型
  message(newVal, oldVal) {
    console.log(`从 ${oldVal} 变为 ${newVal}`)
  },
  
  // 监听对象(需要 deep: true)
  user: {
    handler(newVal) {
      console.log('用户信息变化', newVal)
    },
    deep: true        // 深度监听
  },
  
  // 立即执行
  searchQuery: {
    handler(newVal) {
      this.fetchResults(newVal)
    },
    immediate: true   // 组件创建时立即执行
  }
}

侦听多个数据源

javascript 复制代码
// Vue 2 需要分别侦听
watch: {
  firstName() { this.updateFullName() },
  lastName() { this.updateFullName() }
}

// 或者使用 computed 中转
computed: {
  fullName() {
    return `${this.firstName} ${this.lastName}`
  }
},
watch: {
  fullName(newVal) {
    console.log('全名变化', newVal)
  }
}

深度监听排除某些属性

javascript 复制代码
watch: {
  user: {
    handler(newVal) {
      // 手动处理
    },
    deep: true
  }
}

// 或者使用 computed 排除
computed: {
  userWithoutAge() {
    const { age, ...rest } = this.user
    return rest
  }
},
watch: {
  userWithoutAge(newVal) {
    // 只监听除 age 外的属性
  }
}

5. watch、methods、computed 三者对比

特性 computed watch methods
缓存
异步
需要传参
响应式依赖 可选
使用方式 属性 回调 函数调用

五、组件系统与组件通信

1. Vue 组件概述

什么是组件?

组件是可复用的 Vue 实例,拥有自己的数据、计算属性、方法、生命周期等。

为什么要使用组件?

  • 提高代码复用性
  • 便于维护和测试
  • 实现关注点分离
  • 提高开发效率

2. 组件的创建方式

全局注册

javascript 复制代码
// main.js
Vue.component('my-component', {
  template: '<div>全局组件</div>'
})

局部注册

javascript 复制代码
import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}

单文件组件(SFC)

html 复制代码
<!-- MyComponent.vue -->
<template>
  <div class="my-component">
    {{ message }}
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return { message: 'Hello' }
  }
}
</script>

<style scoped>
.my-component { color: red; }
</style>

3. 组件中 data 为什么是函数?

原因

组件可能被多次实例化,如果 data 是对象,所有实例会共享同一个 data 对象,导致数据相互影响。使用函数可以确保每个实例都有独立的 data 副本。

javascript 复制代码
// ❌ 错误:data 是对象,实例间共享
Vue.component('counter', {
  data: { count: 0 },
  template: '<button @click="count++">{{ count }}</button>'
})

// ✅ 正确:data 是函数,每个实例独立
Vue.component('counter', {
  data() {
    return { count: 0 }
  },
  template: '<button @click="count++">{{ count }}</button>'
})

4. 组件间通信方式总览

通信方式 适用场景 说明
props / $emit 父子组件 最常用
$parent / $children 父子组件 直接访问实例
$refs 父访问子 直接访问子组件实例
provide / inject 祖先-后代 跨层级传递
$attrs / $listeners 隔代传递 属性透传
Event Bus 兄弟组件 事件总线
Vuex 复杂状态 集中式状态管理
$root 任意组件 访问根实例

5. 父子组件通信

父传子:props

html 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <child-component :message="parentMsg" :count="5" />
</template>

<script>
export default {
  data() {
    return { parentMsg: 'Hello from parent' }
  }
}
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>{{ message }} - {{ count }}</div>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      required: true
    },
    count: {
      type: Number,
      default: 0,
      validator(value) {
        return value >= 0
      }
    }
  }
}
</script>

props 验证机制

javascript 复制代码
props: {
  // 基础类型检查
  propA: Number,
  
  // 多个可能的类型
  propB: [String, Number],
  
  // 必填字符串
  propC: {
    type: String,
    required: true
  },
  
  // 带有默认值
  propD: {
    type: Number,
    default: 100
  },
  
  // 带有默认值的对象
  propE: {
    type: Object,
    default() {
      return { message: 'hello' }
    }
  },
  
  // 自定义验证函数
  propF: {
    validator(value) {
      return ['success', 'warning', 'danger'].includes(value)
    }
  }
}

子传父:$emit

html 复制代码
<!-- 子组件 Child.vue -->
<template>
  <button @click="sendToParent">发送数据给父组件</button>
</template>

<script>
export default {
  methods: {
    sendToParent() {
      this.$emit('custom-event', { message: 'Hello parent!' })
    }
  }
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <child-component @custom-event="handleEvent" />
</template>

<script>
export default {
  methods: {
    handleEvent(data) {
      console.log('收到子组件数据', data)
    }
  }
}
</script>

v-model 实现父子双向绑定

html 复制代码
<!-- 父组件 -->
<child-input v-model="parentValue" />

<!-- 子组件 ChildInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  methods: {
    updateValue(e) {
      this.$emit('update:modelValue', e.target.value)
    }
  }
}
</script>

6. 兄弟组件通信

方式一:通过父组件中转

html 复制代码
<!-- 父组件 -->
<template>
  <div>
    <child-a @send-data="handleData" />
    <child-b :data="sharedData" />
  </div>
</template>

<script>
export default {
  data() {
    return { sharedData: '' }
  },
  methods: {
    handleData(data) {
      this.sharedData = data
    }
  }
}
</script>

方式二:Event Bus(事件总线)

javascript 复制代码
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 组件 A 发送事件
EventBus.$emit('custom-event', { data: 'hello' })

// 组件 B 接收事件
EventBus.$on('custom-event', (data) => {
  console.log('收到数据', data)
})

// 组件销毁时移除监听
beforeDestroy() {
  EventBus.$off('custom-event')
}

7. 跨层级通信

provide / inject

javascript 复制代码
// 祖先组件
export default {
  provide() {
    return {
      theme: 'dark',
      updateUser: this.updateUser
    }
  },
  data() {
    return { user: {} }
  },
  methods: {
    updateUser(userData) {
      this.user = userData
    }
  }
}

// 后代组件(无论多深)
export default {
  inject: ['theme', 'updateUser'],
  mounted() {
    console.log('当前主题', this.theme)
    this.updateUser({ name: 'John' })
  }
}

provide/inject 与 props 的区别

对比维度 props provide/inject
传递范围 直接父子 任意层级的祖先-后代
响应式 ✅ 是 默认非响应式(传递对象属性可响应)
适用场景 常规父子通信 插件、主题等跨层级配置

<math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s / attrs / </math>attrs/listeners

html 复制代码
<!-- 父组件传递 -->
<child-a :foo="foo" :bar="bar" @click="handleClick" />

<!-- 子组件 ChildA.vue(透传给孙组件) -->
<template>
  <grand-child v-bind="$attrs" v-on="$listeners" />
</template>

<script>
export default {
  inheritAttrs: false, // 阻止默认行为
  // $attrs 包含所有未声明的 props
  // $listeners 包含所有事件监听器
}
</script>

8. $refs 的使用

html 复制代码
<template>
  <div>
    <input ref="myInput" />
    <child-component ref="myChild" />
    <ul>
      <li v-for="item in list" :key="item.id" ref="items">
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  mounted() {
    // 访问 DOM 元素
    this.$refs.myInput.focus()
    
    // 访问子组件实例
    this.$refs.myChild.childMethod()
    
    // v-for 中的 refs 是数组
    this.$refs.items.forEach(el => {
      console.log(el)
    })
  }
}
</script>

9. 组件中 name 选项的作用

  1. 递归组件:组件调用自身时需要 name
  2. Vue Devtools:调试时显示组件名称
  3. keep-alive 匹配include / exclude 属性依赖 name
  4. 动态组件:注册全局组件时需要 name
javascript 复制代码
export default {
  name: 'TreeMenu',
  // 递归调用自身
  components: {
    TreeMenu
  }
}

10. 动态组件与异步组件

动态组件

html 复制代码
<template>
  <!-- 根据 currentTab 动态切换组件 -->
  <component :is="currentTabComponent"></component>
</template>

<script>
import TabHome from './TabHome.vue'
import TabPosts from './TabPosts.vue'
import TabArchive from './TabArchive.vue'

export default {
  data() {
    return { currentTab: 'Home' }
  },
  computed: {
    currentTabComponent() {
      return {
        Home: TabHome,
        Posts: TabPosts,
        Archive: TabArchive
      }[this.currentTab]
    }
  }
}
</script>

异步组件

javascript 复制代码
// 工厂函数方式
Vue.component('async-component', () => import('./AsyncComponent.vue'))

// 带加载状态
const AsyncComponent = () => ({
  component: import('./AsyncComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// 局部注册
export default {
  components: {
    AsyncComponent: () => import('./AsyncComponent.vue')
  }
}

11. 递归组件

html 复制代码
<!-- TreeItem.vue -->
<template>
  <li>
    <div @click="toggle">{{ item.name }}</div>
    <ul v-show="isOpen" v-if="item.children">
      <tree-item 
        v-for="child in item.children" 
        :key="child.id" 
        :item="child"
      />
    </ul>
  </li>
</template>

<script>
export default {
  name: 'TreeItem', // 必须有 name 才能递归
  props: ['item'],
  data() {
    return { isOpen: false }
  },
  methods: {
    toggle() {
      this.isOpen = !this.isOpen
    }
  }
}
</script>

12. 如何解决 Vue 组件的命名冲突?

javascript 复制代码
// 方式一:使用别名
import { Button as MyButton } from './MyButton.vue'
import { Button } from 'element-ui'

export default {
  components: {
    MyButton,
    ElButton: Button
  }
}

// 方式二:使用 name 选项区分
export default {
  name: 'CustomButton'
}

13. 如何重置组件的 data?

javascript 复制代码
export default {
  data() {
    return {
      form: { name: '', age: 0 },
      isLoading: false
    }
  },
  methods: {
    resetData() {
      // 方式一:重新调用 data 函数
      Object.assign(this.$data, this.$options.data.call(this))
      
      // 方式二:逐个重置
      this.form = { name: '', age: 0 }
      this.isLoading = false
    }
  }
}

六、插槽(Slots)

1. 插槽概述

定义

插槽(Slot)是 Vue 提供的组件内容分发机制,允许父组件向子组件传递内容。

2. 默认插槽

html 复制代码
<!-- 子组件 Alert.vue -->
<template>
  <div class="alert">
    <slot>默认内容(当没有传入内容时显示)</slot>
  </div>
</template>

<!-- 父组件 -->
<alert>
  <p>自定义内容</p>
</alert>

3. 具名插槽

html 复制代码
<!-- 子组件 BaseLayout.vue -->
<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 父组件 -->
<base-layout>
  <template v-slot:header>
    <h1>页面标题</h1>
  </template>
  
  <p>主要内容</p>
  
  <template #footer>
    <p>版权信息</p>
  </template>
</base-layout>

4. 作用域插槽

子组件暴露数据给父组件

html 复制代码
<!-- 子组件 UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <slot :user="user" :index="index">
        {{ user.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: 'John', age: 25 },
        { id: 2, name: 'Jane', age: 30 }
      ]
    }
  }
}
</script>

<!-- 父组件 -->
<user-list>
  <template v-slot:default="slotProps">
    <strong>{{ slotProps.user.name }}</strong>
    <span>年龄: {{ slotProps.user.age }}</span>
  </template>
</user-list>

<!-- 简写 -->
<user-list v-slot="{ user }">
  <strong>{{ user.name }}</strong>
</user-list>

5. 插槽的使用场景

  1. 组件封装:如对话框、弹窗、卡片组件
  2. 列表渲染定制:表格、列表等组件的单元格定制
  3. 布局组件:页面布局的 header、footer、sidebar
  4. 表单组件:表单项的额外内容

七、Vue Router 路由

1. Vue Router 概述

定义

Vue Router 是 Vue.js 官方的路由管理器,用于构建单页应用(SPA),实现页面组件的动态切换。

2. 基本使用步骤

javascript 复制代码
// 1. 定义路由组件
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }

// 2. 定义路由配置
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

// 3. 创建 router 实例
const router = new VueRouter({
  routes
})

// 4. 挂载到 Vue 实例
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

3. 路由模式

模式 URL 示例 原理 特点
hash 模式 http://example.com/#/home 利用 window.location.hash 兼容性好,不需要服务端配置
history 模式 http://example.com/home 利用 HTML5 History API URL 更美观,需要服务端配置

hash 模式原理

javascript 复制代码
// 监听 hash 变化
window.addEventListener('hashchange', () => {
  const hash = window.location.hash.slice(1)
  // 根据 hash 渲染对应组件
  renderComponent(hash)
})

history 模式原理

javascript 复制代码
// 监听浏览器前进/后退
window.addEventListener('popstate', () => {
  const path = window.location.pathname
  // 根据 path 渲染对应组件
  renderComponent(path)
})

// pushState 改变 URL 不刷新页面
history.pushState({}, '', '/new-path')

history 模式服务端配置(Nginx)

nginx 复制代码
location / {
  try_files $uri $uri/ /index.html;
}

选择策略

  • 需要兼容老浏览器 → hash 模式
  • 需要更好 SEO → history 模式(配合 SSR)
  • 简单项目 → hash 模式(无需服务端配置)

4. 动态路由

javascript 复制代码
const routes = [
  // 动态路径参数,以冒号开头
  { path: '/user/:id', component: User }
]

// 匹配 /user/1 → $route.params.id = '1'
// 匹配 /user/2 → $route.params.id = '2'

响应路由参数变化

javascript 复制代码
export default {
  watch: {
    // 路由变化时重新获取数据
    $route(to, from) {
      this.fetchUser(to.params.id)
    }
  },
  // 或使用路由守卫
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.id)
    next()
  }
}

5. 路由传参

方式一:动态路由参数

javascript 复制代码
// 配置
{ path: '/user/:id', component: User }

// 跳转
this.$router.push('/user/123')

// 获取
this.$route.params.id // '123'

方式二:query 参数

javascript 复制代码
// 跳转
this.$router.push({ path: '/search', query: { keyword: 'vue' } })

// 获取
this.$route.query.keyword // 'vue'

方式三:params(命名路由)

javascript 复制代码
// 配置
{ path: '/user/:id', name: 'user', component: User }

// 跳转
this.$router.push({ name: 'user', params: { id: '123' } })

// 获取
this.$route.params.id // '123'

6. 嵌套路由

javascript 复制代码
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        path: 'profile', // 相对路径
        component: UserProfile
      },
      {
        path: 'posts',
        component: UserPosts
      }
    ]
  }
]
html 复制代码
<!-- User.vue 中需要放置 router-view -->
<template>
  <div>
    <h2>User {{ $route.params.id }}</h2>
    <router-view></router-view>
  </div>
</template>

7. 编程式导航

javascript 复制代码
// 字符串路径
this.$router.push('/home')

// 对象路径
this.$router.push({ path: '/home' })

// 命名路由
this.$router.push({ name: 'user', params: { id: '123' } })

// 带查询参数
this.$router.push({ path: '/search', query: { q: 'vue' } })

// 前进/后退
this.$router.go(1)    // 前进一条记录
this.$router.go(-1)   // 后退一条记录
this.$router.forward()  // 前进
this.$router.back()     // 后退

// 替换当前记录(不留下历史)
this.$router.replace('/home')

8. 路由守卫

全局守卫

javascript 复制代码
const router = new VueRouter({ ... })

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 检查登录状态
  const token = localStorage.getItem('token')
  if (to.meta.requiresAuth && !token) {
    next('/login')
  } else {
    next()
  }
})

// 全局解析守卫
router.beforeResolve((to, from, next) => {
  next()
})

// 全局后置钩子
router.afterEach((to, from) => {
  // 不能调用 next()
  console.log('导航完成')
})

路由独享守卫

javascript 复制代码
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      // 只在进入该路由时触发
      next()
    }
  }
]

组件内守卫

javascript 复制代码
export default {
  // 进入组件前
  beforeRouteEnter(to, from, next) {
    // 不能获取组件实例 this
    next(vm => {
      // 组件实例已创建,可以访问 vm
    })
  },
  
  // 路由更新时
  beforeRouteUpdate(to, from, next) {
    // 当前路由改变但组件被复用时调用
    next()
  },
  
  // 离开组件前
  beforeRouteLeave(to, from, next) {
    // 可以访问 this
    const answer = window.confirm('确定离开吗?')
    if (answer) {
      next()
    } else {
      next(false)
    }
  }
}

守卫执行顺序

markdown 复制代码
1. 导航被触发
2. 在失活组件中调用 beforeRouteLeave
3. 调用全局 beforeEach
4. 在重用组件中调用 beforeRouteUpdate
5. 在路由配置中调用 beforeEnter
6. 解析异步路由组件
7. 在被激活组件中调用 beforeRouteEnter
8. 调用全局 beforeResolve
9. 导航被确认
10. 调用全局 afterEach
11. 触发 DOM 更新
12. 调用 beforeRouteEnter 的 next 回调

9. 路由懒加载

定义

路由懒加载是将路由对应的组件打包成独立的 chunk,在访问该路由时才加载对应组件,从而减少首屏加载时间。

实现方式

javascript 复制代码
// Vue CLI 项目(使用动态 import)
const routes = [
  {
    path: '/about',
    component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
  },
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

分组懒加载

javascript 复制代码
// 将多个组件打包到同一个 chunk
const routes = [
  {
    path: '/user',
    component: () => import(/* webpackChunkName: "user" */ './views/User.vue')
  },
  {
    path: '/profile',
    component: () => import(/* webpackChunkName: "user" */ './views/Profile.vue')
  }
]

10. <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r 与 router 与 </math>router与route 的区别

对比维度 $router $route
类型 VueRouter 实例 当前路由信息对象
用途 导航(跳转、前进、后退) 获取路由信息(参数、路径等)
常用方法/属性 push()replace()go()back() paramsquerypathnamemeta
javascript 复制代码
// $router:用于导航
this.$router.push('/home')
this.$router.replace('/login')
this.$router.go(-1)

// $route:用于获取信息
this.$route.params.id      // 动态参数
this.$route.query.keyword  // 查询参数
this.$route.path           // 当前路径
this.$route.name           // 路由名称
this.$route.meta           // 元信息

11. 路由元信息(meta)

javascript 复制代码
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: '管理后台',
      roles: ['admin', 'super_admin']
    }
  }
]

// 使用
router.beforeEach((to, from, next) => {
  document.title = to.meta.title || '默认标题'
  
  if (to.meta.requiresAuth) {
    // 检查权限
  }
  
  next()
})

八、Vuex 状态管理

1. Vuex 概述

定义

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

应用场景

  • 多个组件共享状态
  • 不同组件需要更改同一状态
  • 状态变化逻辑复杂

Vuex 与 localStorage 的区别

对比维度 Vuex localStorage
存储位置 内存 浏览器本地存储
响应式 ✅ 是 ❌ 否
持久化 ❌ 刷新丢失(需配合插件) ✅ 持久化
适用场景 组件间状态共享 数据持久化
数据量 适合复杂状态 适合简单键值对

2. Vuex 核心概念

scss 复制代码
┌──────────────────────────────────────────┐
│                  Vuex                     │
│                                           │
│  Component                                │
│     │                                     │
│     │ dispatch                            │
│     ▼                                     │
│  ┌─────────┐     ┌─────────────────────┐  │
│  │ Actions │────▶│    Mutations        │  │
│  │ (异步)  │     │ (同步,修改 state)   │  │
│  └─────────┘     └─────────────────────┘  │
│                            │               │
│                            │ commit        │
│                            ▼               │
│                      ┌─────────┐           │
│                      │  State  │           │
│                      └─────────┘           │
│                            │               │
│                            │ getters       │
│                            ▼               │
│                      ┌─────────┐           │
│                      │Getters  │           │
│                      └─────────┘           │
│                            │               │
│                            ▼               │
│                      Component             │
└──────────────────────────────────────────┘

数据流

复制代码
组件触发 Action → Action 执行异步操作 → Action 提交 Mutation → Mutation 修改 State → State 变化触发组件更新

3. State

定义

State 是 Vuex 的单一状态树,包含应用中所有的状态数据。

javascript 复制代码
// store.js
const store = new Vuex.Store({
  state: {
    count: 0,
    user: null,
    todos: []
  }
})

// 组件中访问
computed: {
  count() {
    return this.$store.state.count
  }
}

// 使用 mapState 辅助函数
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['count', 'user']),
    ...mapState({
      countAlias: 'count',
      countPlusLocal(state) {
        return state.count + this.localCount
      }
    })
  }
}

4. Getter

定义

Getter 类似于计算属性,用于从 state 中派生出一些状态。

javascript 复制代码
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '学习 Vue', done: true },
      { id: 2, text: '学习 Vuex', done: false }
    ]
  },
  getters: {
    // 基础 getter
    doneTodos(state) {
      return state.todos.filter(todo => todo.done)
    },
    
    // 接收其他 getter
    doneTodosCount(state, getters) {
      return getters.doneTodos.length
    },
    
    // 返回函数(可传参)
    getTodoById(state) {
      return id => state.todos.find(todo => todo.id === id)
    }
  }
})

// 组件中使用
computed: {
  ...mapGetters(['doneTodos', 'doneTodosCount'])
}

5. Mutation

定义

Mutation 是更改 Vuex store 中状态的唯一方法,必须是同步函数。

javascript 复制代码
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // 基础用法
    increment(state) {
      state.count++
    },
    
    // 接收载荷(参数)
    incrementBy(state, payload) {
      state.count += payload.amount
    },
    
    // 对象风格的提交
    incrementBy(state, payload) {
      state.count += payload.amount
    }
  }
})

// 组件中触发
this.$store.commit('increment')
this.$store.commit('incrementBy', { amount: 10 })

// 使用 mapMutations
import { mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations(['increment', 'incrementBy'])
  }
}

Mutation 必须是同步函数

如果 Mutation 包含异步操作,Devtools 无法正确追踪状态变化。

6. Action

定义

Action 类似于 Mutation,但可以包含异步操作,不能直接修改 state,需要提交 Mutation。

javascript 复制代码
const store = new Vuex.Store({
  state: {
    user: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
    }
  },
  actions: {
    // 基础用法
    async login({ commit }, credentials) {
      const user = await api.login(credentials)
      commit('setUser', user)
    },
    
    // 解构上下文
    async fetchData({ commit, state, getters, dispatch }) {
      const data = await api.getData()
      commit('setData', data)
    },
    
    // 组合多个 Action
    async checkout({ commit, dispatch }, cart) {
      try {
        await api.checkout(cart)
        dispatch('clearCart')
        commit('setOrderSuccess', true)
      } catch (error) {
        commit('setOrderError', error)
      }
    }
  }
})

// 组件中触发
this.$store.dispatch('login', { username: 'user', password: '123' })

// 使用 mapActions
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions(['login', 'fetchData'])
  }
}

7. Module

定义

当应用变得复杂时,store 对象会变得臃肿。Vuex 允许将 store 分割成模块(module)。

javascript 复制代码
// modules/user.js
const user = {
  namespaced: true, // 开启命名空间
  state: () => ({
    name: '',
    token: ''
  }),
  mutations: {
    setName(state, name) {
      state.name = name
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const res = await api.login(credentials)
      commit('setName', res.name)
    }
  },
  getters: {
    isLoggedIn(state) {
      return !!state.token
    }
  }
}

// modules/cart.js
const cart = {
  namespaced: true,
  state: () => ({
    items: []
  }),
  mutations: {
    addItem(state, item) {
      state.items.push(item)
    }
  }
}

// store/index.js
const store = new Vuex.Store({
  modules: {
    user,
    cart
  }
})

// 组件中使用
this.$store.state.user.name
this.$store.getters['user/isLoggedIn']
this.$store.dispatch('user/login', credentials)
this.$store.commit('cart/addItem', item)

// 使用辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('user')

export default {
  computed: {
    ...mapState(['name', 'token'])
  },
  methods: {
    ...mapActions(['login'])
  }
}

8. Vuex 与全局事件总线的区别

对比维度 Vuex Event Bus
状态追踪 Devtools 可追踪 无法追踪
响应式 ✅ 是 ❌ 否
调试 时间旅行调试 难以调试
适用场景 复杂状态管理 简单事件通知
规则 严格的单向数据流 随意触发

9. Vuex 严格模式

javascript 复制代码
const store = new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  // ...
})

作用:在严格模式下,任何在 mutation 之外修改 state 的行为都会抛出错误。


九、响应式原理与核心原理

1. Vue 响应式原理(Vue 2)

核心机制

Vue 2 使用 Object.defineProperty 实现数据劫持,结合发布-订阅模式实现响应式。

实现流程

markdown 复制代码
1. 数据初始化(data 选项)
   ↓
2. 遍历 data 的每个属性
   ↓
3. 使用 Object.defineProperty 将属性转为 getter/setter
   ↓
4. getter 中进行依赖收集(Dep.depend)
   ↓
5. setter 中派发更新(Dep.notify)
   ↓
6. Watcher 接收更新通知,执行回调更新视图

简化源码解读

javascript 复制代码
// 1. Observer:将数据转为响应式
class Observer {
  constructor(data) {
    this.walk(data)
  }
  
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

// 2. defineReactive:定义响应式属性
function defineReactive(obj, key, val) {
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 派发更新
      dep.notify()
    }
  })
}

// 3. Dep:依赖收集器
class Dep {
  constructor() {
    this.subs = []
  }
  
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

// 4. Watcher:订阅者
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.getter = typeof expOrFn === 'function' ? expOrFn : () => vm[expOrFn]
    this.value = this.get()
  }
  
  get() {
    Dep.target = this
    const value = this.getter.call(this.vm)
    Dep.target = null
    return value
  }
  
  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

依赖收集流程

scss 复制代码
组件渲染 → 访问响应式数据 → 触发 getter → Dep.depend() 收集 Watcher → 数据变化 → 触发 setter → Dep.notify() → Watcher.update() → 重新渲染

2. Vue 3 响应式原理(Proxy)

Vue 3 改用 Proxy 的原因

  1. Object.defineProperty 只能劫持已有属性,无法检测属性添加/删除
  2. 无法检测数组索引和长度变化
  3. Proxy 性能更好

Proxy 实现

javascript 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      // 深层响应式
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      return result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      // 派发更新
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    }
  })
}

3. Vue 双向绑定原理

实现机制

markdown 复制代码
1. 响应式系统:Object.defineProperty/Proxy 数据劫持
   ↓
2. 模板编译:解析模板中的指令和插值表达式
   ↓
3. 依赖收集:视图与数据建立依赖关系
   ↓
4. 派发更新:数据变化时自动更新视图
   ↓
5. v-model:表单元素上实现双向绑定

完整流程

csharp 复制代码
┌─────────────────────────────────────────────┐
│              双向绑定原理                      │
│                                              │
│  View ←─── v-model ───→ ViewModel            │
│   │                           │              │
│   │                           │              │
│   ▼                           ▼              │
│  DOM 事件                   响应式数据         │
│   │                           │              │
│   │                           │              │
│   ▼                           ▼              │
│  input event → 更新数据 → setter → 通知更新   │
│                                              │
└─────────────────────────────────────────────┘

4. Vue 异步更新队列

原理

Vue 在更新 DOM 时是异步执行的。当侦听到数据变化时,会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入队列一次。

作用

  • 避免不必要的计算和 DOM 操作
  • 提升性能

流程

复制代码
数据变化 → Watcher 入队 → nextTick 执行 → 批量更新 DOM

源码简析

javascript 复制代码
// 简化的异步更新逻辑
const queue = []
let pending = false

function queueWatcher(watcher) {
  queue.push(watcher)
  
  if (!pending) {
    pending = true
    // 使用微任务优先
    Promise.resolve().then(flushQueue)
  }
}

function flushQueue() {
  queue.forEach(watcher => {
    watcher.run()
  })
  queue = []
  pending = false
}

5. Vue.nextTick

定义
nextTick 用于在下次 DOM 更新循环结束之后执行延迟回调。

使用场景

javascript 复制代码
export default {
  data() {
    return { message: 'Hello' }
  },
  methods: {
    async updateMessage() {
      this.message = 'New message'
      
      // 此时 DOM 还未更新
      console.log(this.$refs.msg.textContent) // 还是旧值
      
      // 等待 DOM 更新
      await this.$nextTick()
      console.log(this.$refs.msg.textContent) // 新值
    }
  }
}

实现原理

javascript 复制代码
// Vue 内部实现(降级策略)
let timerFunc

if (typeof Promise !== 'undefined') {
  // 优先使用 Promise(微任务)
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (typeof MutationObserver !== 'undefined') {
  // 使用 MutationObserver
} else if (typeof setImmediate !== 'undefined') {
  // 使用 setImmediate
} else {
  // 最后使用 setTimeout(宏任务)
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

6. Vue 虚拟 DOM

定义

虚拟 DOM 是用 JavaScript 对象来描述真实 DOM 结构的一种抽象。

为什么需要虚拟 DOM?

  1. 减少 DOM 操作,提升性能
  2. 跨平台(如 Weex、服务端渲染)
  3. 提供 diff 算法基础

VNode 结构

javascript 复制代码
class VNode {
  constructor(tag, data, children, text, elm, context) {
    this.tag = tag           // 标签名
    this.data = data         // 属性、事件等
    this.children = children // 子节点
    this.text = text         // 文本内容
    this.elm = elm           // 对应的真实 DOM
    this.key = data && data.key
  }
}

// 示例
{
  tag: 'div',
  data: {
    attrs: { id: 'app' },
    class: { active: true }
  },
  children: [
    {
      tag: 'span',
      text: 'Hello Vue'
    }
  ]
}

7. Vue diff 算法

定义

diff 算法用于比较新旧虚拟 DOM 树的差异,最小化 DOM 操作。

核心策略

  1. 同层比较,不跨级比较
  2. 使用 key 优化列表渲染
  3. 双端比较(Vue 2)

Vue 2 diff 流程

markdown 复制代码
1. 新旧节点比较
   ↓
2. 如果是相同节点,递归比较子节点
   ↓
3. 子节点比较:
   - 旧头与新头比较
   - 旧尾与新尾比较
   - 旧头与新尾比较
   - 旧尾与新头比较
   - 都不匹配时,用 key 查找
   ↓
4. 批量更新 DOM

源码简析

javascript 复制代码
function patch(oldVnode, vnode) {
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode)
  } else {
    // 不同节点,直接替换
    const elm = oldVnode.parentNode
    const parentElm = elm.parentNode
    createElm(vnode)
    parentElm.insertBefore(vnode.elm, elm)
    parentElm.removeChild(elm)
  }
}

function patchVnode(oldVnode, vnode) {
  const oldCh = oldVnode.children
  const newCh = vnode.children
  
  if (oldCh && newCh && oldCh !== newCh) {
    // 比较子节点列表
    updateChildren(oldCh, newCh)
  } else if (newCh) {
    // 新增子节点
    addVnodes(newCh)
  } else if (oldCh) {
    // 删除子节点
    removeVnodes(oldCh)
  }
}

8. key 的作用

为什么列表渲染必须使用 key?

  1. 唯一标识节点:帮助 diff 算法识别节点身份
  2. 复用节点:提高渲染性能
  3. 维持状态:避免组件状态混乱

无 key 的问题

html 复制代码
<!-- ❌ 不推荐:无 key -->
<li v-for="item in list">{{ item.name }}</li>

<!-- ✅ 推荐:使用唯一 key -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>

key 的最佳实践

  • ✅ 使用唯一且稳定的 ID(如数据库主键)
  • ❌ 不要使用 index 作为 key(列表顺序变化时会出错)
  • ❌ 不要使用随机数(每次渲染都不同)
javascript 复制代码
// ❌ 错误:使用 index
<li v-for="(item, index) in list" :key="index">

// ❌ 错误:使用随机数
<li v-for="item in list" :key="Math.random()">

// ✅ 正确:使用唯一 ID
<li v-for="item in list" :key="item.id">

9. Vue 模板编译过程

编译三阶段

markdown 复制代码
1. 解析(Parse):将模板字符串转换为 AST(抽象语法树)
   ↓
2. 优化(Optimize):标记静态节点,优化渲染性能
   ↓
3. 生成(Generate):将 AST 转换为 render 函数

源码流程

javascript 复制代码
// 1. parse:模板 → AST
function parse(template) {
  // 词法分析:解析标签、属性、文本等
  // 构建 AST 树
  return ast
}

// 2. optimize:标记静态节点
function optimize(ast) {
  // 遍历 AST,标记静态节点
  // 静态节点不会变化,渲染时可跳过
  markStatic(ast)
}

// 3. generate:AST → render 函数
function generate(ast) {
  const code = generateCode(ast)
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: []
  }
}

AST 节点示例

javascript 复制代码
{
  type: 1,
  tag: 'div',
  attrsList: [{ name: 'id', value: 'app' }],
  attrsMap: { id: 'app' },
  parent: undefined,
  children: [
    {
      type: 2,
      expression: "'Hello ' + name",
      text: 'Hello {{name}}',
      tokens: ['Hello ', { '@binding': 'name' }]
    }
  ]
}

10. Vue 渲染过程

markdown 复制代码
1. 模板编译 → render 函数
   ↓
2. 执行 render 函数 → VNode
   ↓
3. patch 函数:VNode → 真实 DOM
   ↓
4. 数据变化 → 重新执行 render → 新 VNode
   ↓
5. diff 算法:新旧 VNode 比较
   ↓
6. 最小化 DOM 操作

十、keep-alive 组件

1. keep-alive 的作用

定义
<keep-alive> 是 Vue 的内置组件,用于缓存组件实例,避免重复渲染。

使用场景

  • 列表页→详情页→返回列表页(保留滚动位置、筛选状态)
  • 标签页切换
  • 需要保留组件状态的场景

2. keep-alive 的属性

属性 类型 说明
include String/RegExp/Array 只有名称匹配的组件会被缓存
exclude String/RegExp/Array 名称匹配的组件不会被缓存
max Number 最多可以缓存多少组件实例
html 复制代码
<!-- 缓存指定组件 -->
<keep-alive include="Home,About">
  <component :is="currentComponent" />
</keep-alive>

<!-- 使用正则 -->
<keep-alive :include="/^app-/">
  <router-view />
</keep-alive>

<!-- 排除某些组件 -->
<keep-alive exclude="NoCache">
  <router-view />
</keep-alive>

<!-- 限制最大缓存数 -->
<keep-alive :max="10">
  <router-view />
</keep-alive>

3. keep-alive 的生命周期变化

被 keep-alive 缓存的组件:

  • 不会触发 beforeDestroydestroyed
  • 新增 activateddeactivated 钩子
javascript 复制代码
export default {
  activated() {
    console.log('组件被激活(进入缓存)')
    // 恢复状态
  },
  deactivated() {
    console.log('组件被停用(退出缓存)')
    // 保存状态
  }
}

4. keep-alive 实现原理

javascript 复制代码
// 简化版实现
export default {
  name: 'KeepAlive',
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: Number
  },
  created() {
    this.cache = Object.create(null) // 缓存对象
    this.keys = [] // 缓存的 key
  },
  destroyed() {
    // 销毁所有缓存
    Object.keys(this.cache).forEach(key => {
      pruneCacheEntry(this.cache[key])
    })
  },
  render() {
    const slot = this.$slots.default
    const vnode = slot[0]
    const name = vnode.componentOptions.name
    
    // 检查是否需要缓存
    if (this.shouldCache(name)) {
      const key = vnode.key == null
        ? vnode.componentOptions.Ctor.cid + '::' + vnode.tag
        : vnode.key
      
      if (this.cache[key]) {
        vnode.componentInstance = this.cache[key].componentInstance
      } else {
        this.cache[key] = vnode
        this.keys.push(key)
        
        // 超过 max 时,清理最久未使用的缓存
        if (this.max && this.keys.length > this.max) {
          pruneCacheEntry(this.cache[this.keys[0]])
        }
      }
      
      vnode.data.keepAlive = true
    }
    
    return vnode
  }
}

十一、性能优化

1. Vue 性能优化总览

优化方向 具体方法
代码层面 合理使用 computed、v-show/v-if、v-for 绑定 key
组件层面 组件懒加载、keep-alive 缓存、函数式组件
构建层面 路由懒加载、代码分割、Tree Shaking
资源层面 图片优化、CDN 加速、gzip 压缩
渲染层面 虚拟滚动、防抖节流、避免 v-if 与 v-for 同时使用

2. 首屏加载优化

javascript 复制代码
// 1. 路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('./views/Home.vue')
  }
]

// 2. 组件按需加载
import { Button, Input } from 'element-ui'
Vue.use(Button)
Vue.use(Input)

// 3. CDN 外部化
// vue.config.js
module.exports = {
  configureWebpack: {
    externals: {
      vue: 'Vue',
      'vue-router': 'VueRouter'
    }
  }
}

// 4. 图片懒加载
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)

<img v-lazy="imageSrc">

3. 长列表优化

虚拟滚动

html 复制代码
<template>
  <div class="list-container" ref="container" @scroll="handleScroll">
    <div class="list-spacer" :style="{ height: totalHeight + 'px' }">
      <div 
        v-for="item in visibleItems" 
        :key="item.id"
        :style="{ transform: `translateY(${item.top}px)` }"
        class="list-item"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ['items'],
  data() {
    return {
      itemHeight: 50,
      startIndex: 0,
      visibleCount: 20
    }
  },
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight
    },
    visibleItems() {
      const end = Math.min(this.startIndex + this.visibleCount, this.items.length)
      return this.items.slice(this.startIndex, end).map((item, i) => ({
        ...item,
        top: (this.startIndex + i) * this.itemHeight
      }))
    }
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.container.scrollTop
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
    }
  }
}
</script>

4. v-memo 性能优化(Vue 3)

html 复制代码
<!-- 只有当 item.id 或 item.selected 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
  {{ item.name }}
</div>

5. 避免不必要的重新渲染

javascript 复制代码
// ✅ 使用 Object.freeze() 冻结不需要响应式的大对象
export default {
  data() {
    return {
      // 大量静态数据不需要响应式
      hugeList: Object.freeze(largeDataArray)
    }
  }
}

// ✅ 合理使用 v-if 和 v-show
// 频繁切换用 v-show,不频繁切换用 v-if

// ✅ 组件拆分,减少渲染范围
// 将大组件拆分为小组件,精准控制更新范围

6. 组件懒加载

javascript 复制代码
// 方式一:异步组件
components: {
  HeavyComponent: () => import('./HeavyComponent.vue')
}

// 方式二:带加载状态
const HeavyComponent = () => ({
  component: import('./HeavyComponent.vue'),
  loading: LoadingSpinner,
  error: ErrorComponent,
  delay: 200,
  timeout: 5000
})

十二、Mixins 混入

1. Mixins 概述

定义

Mixins 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。

2. Mixins 的使用

javascript 复制代码
// mixin.js
export const myMixin = {
  data() {
    return {
      mixinData: 'hello'
    }
  },
  created() {
    console.log('mixin created')
  },
  methods: {
    mixinMethod() {
      console.log('mixin method')
    }
  }
}

// 组件中使用
import { myMixin } from './mixin'

export default {
  mixins: [myMixin],
  created() {
    console.log('component created')
  }
}

3. 选项合并策略

选项类型 合并策略
data 递归合并,组件数据优先
生命周期钩子 合并为数组,先执行 mixin,再执行组件
methods/components/directives 对象合并,组件选项优先

4. 全局 Mixin

javascript 复制代码
// main.js
Vue.mixin({
  created() {
    console.log('全局 mixin')
  }
})

⚠️ 全局 mixin 慎用:会影响所有 Vue 实例,可能导致难以追踪的 bug。

5. Mixins 的优缺点

优点

  • 代码复用
  • 逻辑抽离

缺点

  • 命名冲突
  • 来源不清晰(不清楚方法来自哪个 mixin)
  • 多 mixin 时依赖关系复杂

Vue 3 替代方案:组合式 API(Composition API)

javascript 复制代码
// Vue 3 中推荐使用 Composables
import { useMouse, useFetch } from './composables'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('/api/data')
    return { x, y, data, error }
  }
}

十三、自定义指令深入

1. 自定义指令钩子函数

javascript 复制代码
Vue.directive('my-directive', {
  // 只调用一次,指令第一次绑定到元素时
  bind(el, binding, vnode) {
    // 可以进行一次性的初始化设置
  },
  
  // 被绑定元素插入父节点时调用
  inserted(el, binding, vnode) {
    // 可以操作父节点
  },
  
  // 所在组件的 VNode 更新时调用
  update(el, binding, vnode, oldVnode) {
    // 可能发生在其子 VNode 更新之前
  },
  
  // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  componentUpdated(el, binding, vnode, oldVnode) {
    // 可以访问更新后的 DOM
  },
  
  // 只调用一次,指令与元素解绑时
  unbind(el, binding, vnode) {
    // 清理工作
  }
})

2. 实战示例

防抖指令

javascript 复制代码
Vue.directive('debounce-click', {
  bind(el, binding) {
    let timeoutId = null
    
    el.addEventListener('click', (e) => {
      if (timeoutId) return
      
      binding.value(e)
      
      timeoutId = setTimeout(() => {
        timeoutId = null
      }, binding.arg || 500)
    })
  }
})

// 使用
<button v-debounce-click:1000="handleSubmit">提交</button>

权限指令

javascript 复制代码
Vue.directive('permission', {
  bind(el, binding) {
    const permissions = store.getters.permissions
    if (!permissions.includes(binding.value)) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  }
})

// 使用
<button v-permission="'admin:delete'">删除</button>

十四、过滤器(Filters)

1. 过滤器概述

定义

过滤器用于文本格式转换,可在插值表达式和 v-bind 表达式中使用。

⚠️ 注意:Vue 3 中已移除过滤器,推荐使用计算属性或方法替代。

2. 使用方式

全局注册

javascript 复制代码
Vue.filter('capitalize', function(value) {
  if (!value) return ''
  return value.toString().charAt(0).toUpperCase() + value.slice(1)
})

局部注册

javascript 复制代码
export default {
  filters: {
    capitalize(value) {
      if (!value) return ''
      return value.charAt(0).toUpperCase() + value.slice(1)
    },
    formatDate(value, format = 'YYYY-MM-DD') {
      return dayjs(value).format(format)
    }
  }
}

使用

html 复制代码
<!-- 插值表达式 -->
{{ message | capitalize }}

<!-- v-bind -->
<div :title="message | capitalize"></div>

<!-- 串联 -->
{{ message | filterA | filterB }}

<!-- 传参 -->
{{ date | formatDate('YYYY/MM/DD') }}

十五、样式隔离

1. Scoped CSS

原理

Vue 为 scoped 样式添加唯一的属性选择器来实现样式隔离。

html 复制代码
<style scoped>
.title {
  color: red;
}
</style>

<!-- 编译后 -->
<style>
.title[data-v-f3f3eg9] {
  color: red;
}
</style>

<!-- 模板编译后 -->
<h1 data-v-f3f3eg9 class="title">标题</h1>

2. 深度选择器

html 复制代码
<style scoped>
/* 修改子组件样式 */
.parent >>> .child {
  color: red;
}

/* SCSS/LESS 使用 /deep/ 或 ::v-deep */
.parent /deep/ .child {
  color: red;
}

.parent ::v-deep .child {
  color: red;
}

/* Vue 3 */
:deep(.child) {
  color: red;
}
</style>

3. CSS Modules

html 复制代码
<template>
  <div :class="$style.container">
    <p :class="$style.title">标题</p>
  </div>
</template>

<style module>
.container {
  padding: 20px;
}
.title {
  color: red;
}
</style>

十六、过渡动画

1. 过渡基础

html 复制代码
<template>
  <transition name="fade">
    <p v-if="show">Hello</p>
  </transition>
</template>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

2. 过渡类名

类名 说明
v-enter 进入过渡的开始状态
v-enter-active 进入过渡的生效状态
v-enter-to 进入过渡的结束状态
v-leave 离开过渡的开始状态
v-leave-active 离开过渡的生效状态
v-leave-to 离开过渡的结束状态

3. 使用第三方动画库

html 复制代码
<transition
  enter-active-class="animate__animated animate__fadeIn"
  leave-active-class="animate__animated animate__fadeOut"
>
  <p v-if="show">Hello</p>
</transition>

十七、实战应用

1. 权限控制系统

路由级权限

javascript 复制代码
// router/index.js
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: { requiresAuth: true, roles: ['admin'] }
  }
]

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  
  if (to.meta.requiresAuth && !token) {
    next('/login')
    return
  }
  
  if (to.meta.roles) {
    const userRoles = store.getters.userRoles
    const hasPermission = to.meta.roles.some(role => userRoles.includes(role))
    if (!hasPermission) {
      next('/403')
      return
    }
  }
  
  next()
})

按钮级权限

javascript 复制代码
// 自定义指令
Vue.directive('permission', {
  bind(el, binding) {
    const permissions = store.getters.permissions
    if (!permissions.includes(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  }
})

// 使用
<button v-permission="'user:delete'">删除用户</button>

2. 国际化(i18n)

javascript 复制代码
// main.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

const i18n = new VueI18n({
  locale: 'zh-CN',
  messages: {
    'zh-CN': {
      welcome: '欢迎',
      hello: '你好,{name}'
    },
    'en-US': {
      welcome: 'Welcome',
      hello: 'Hello, {name}'
    }
  }
})

new Vue({ i18n }).$mount('#app')
html 复制代码
<!-- 使用 -->
<template>
  <div>
    {{ $t('welcome') }}
    {{ $t('hello', { name: 'John' }) }}
  </div>
</template>

3. 全局错误处理

javascript 复制代码
// main.js
Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue 错误:', err)
  console.error('组件:', vm)
  console.error('错误信息:', info)
  
  // 上报错误到监控系统
  reportError({
    message: err.message,
    stack: err.stack,
    component: vm.$options.name,
    info
  })
}

4. 跨域请求处理

开发环境:Vue CLI 代理

javascript 复制代码
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

生产环境:Nginx 配置

nginx 复制代码
server {
  location /api/ {
    proxy_pass http://backend-server:3000/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

5. SEO 优化

服务端渲染(SSR)

javascript 复制代码
// 使用 Nuxt.js 实现 SSR
// nuxt.config.js
export default {
  target: 'server',
  head: {
    title: 'My App',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'My Vue App' }
    ]
  }
}

预渲染

javascript 复制代码
// 使用 prerender-spa-plugin
const PrerenderSPAPlugin = require('prerender-spa-plugin')

module.exports = {
  configureWebpack: {
    plugins: [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        routes: ['/', '/about', '/contact']
      })
    ]
  }
}

6. SPA 的优缺点

优点

  • 用户体验好(页面无刷新)
  • 前后端分离,开发效率高
  • 减轻服务器压力

缺点

  • 首屏加载慢
  • SEO 不友好
  • 前进/后退需要额外处理
  • 浏览器兼容性有限

7. 异步请求处理

html 复制代码
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>{{ data }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null,
      loading: false,
      error: null
    }
  },
  async created() {
    try {
      this.loading = true
      this.data = await this.fetchData()
    } catch (err) {
      this.error = err.message
    } finally {
      this.loading = false
    }
  }
}
</script>

十八、Vue API 参考

1. 全局 API

API 说明 示例
Vue.extend 创建子类构造器 const Profile = Vue.extend({...})
Vue.set 添加响应式属性 Vue.set(obj, 'newProp', value)
Vue.delete 删除响应式属性 Vue.delete(obj, 'prop')
Vue.directive 注册指令 Vue.directive('focus', {...})
Vue.filter 注册过滤器 Vue.filter('format', fn)
Vue.component 注册组件 Vue.component('my-comp', {...})
Vue.use 安装插件 Vue.use(VueRouter)
Vue.mixin 全局混入 Vue.mixin({...})
Vue.compile 编译模板 const res = Vue.compile(template)
Vue.nextTick 延迟回调 Vue.nextTick(cb)
Vue.observable 创建响应式对象 const state = Vue.observable({})

2. Vue.config 配置

javascript 复制代码
Vue.config.silent = true           // 取消所有日志和警告
Vue.config.devtools = true         // 启用 devtools
Vue.config.errorHandler = fn       // 全局错误处理
Vue.config.warnHandler = fn        // 自定义警告处理
Vue.config.productionTip = false   // 取消生产环境提示
Vue.config.performance = true      // 启用性能追踪

3. 实例属性

属性 说明
vm.$data 响应式数据对象
vm.$props 当前组件接收到的 props
vm.$el 根 DOM 元素
vm.$options 初始化选项
vm.$parent 父实例
vm.$root 根实例
vm.$children 子实例数组
vm.$refs 注册过 ref 的所有元素和组件
vm.$attrs 未被 prop 识别的属性
vm.$listeners 监听器对象
vm.$slots 插槽内容
vm.$scopedSlots 作用域插槽
vm.$route 当前路由信息
vm.$router 路由实例

4. 实例方法

方法 说明
vm.$watch 监听数据变化
vm.$set 添加响应式属性
vm.$delete 删除响应式属性
vm.$on 监听事件
vm.$once 监听一次事件
vm.$off 移除事件监听
vm.$emit 触发事件
vm.$mount 手动挂载
vm.$forceUpdate 强制重新渲染
vm.$nextTick DOM 更新后回调
vm.$destroy 销毁实例

十九、Vue CLI 与项目构建

1. Vue CLI 创建项目

bash 复制代码
# 安装 Vue CLI
npm install -g @vue/cli

# 创建项目
vue create my-project

# 选择预设
> Default ([Vue 2] babel, eslint)
  Default ([Vue 3] babel, eslint)
  Manually select features

# 手动选择特性
(*) Babel
(*) TypeScript
(*) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
(*) Unit Testing
(*) E2E Testing

2. Vue 项目结构

csharp 复制代码
my-project/
├── public/
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── assets/         # 静态资源
│   ├── components/     # 公共组件
│   ├── views/          # 页面组件
│   ├── router/         # 路由配置
│   ├── store/          # Vuex 状态管理
│   ├── utils/          # 工具函数
│   ├── api/            # API 接口
│   ├── styles/         # 全局样式
│   ├── App.vue         # 根组件
│   └── main.js         # 入口文件
├── tests/              # 测试文件
├── .env.development    # 开发环境变量
├── .env.production     # 生产环境变量
├── vue.config.js       # Vue CLI 配置
├── package.json
└── README.md

3. 环境变量

bash 复制代码
# .env.development
VUE_APP_API_URL=http://localhost:3000/api
VUE_APP_TITLE=开发环境

# .env.production
VUE_APP_API_URL=https://api.example.com
VUE_APP_TITLE=生产环境
javascript 复制代码
// 使用
console.log(process.env.VUE_APP_API_URL)

4. 打包优化

javascript 复制代码
// vue.config.js
module.exports = {
  // 关闭生产环境 Source Map
  productionSourceMap: false,
  
  configureWebpack: {
    // 代码分割
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            name: 'chunk-vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          }
        }
      }
    }
  },
  
  // 开启 gzip 压缩
  chainWebpack: config => {
    config.plugin('compression-webpack-plugin')
      .use(require('compression-webpack-plugin'), [{
        algorithm: 'gzip',
        test: /\.js$|\.css$|\.html$/,
        threshold: 10240,
        minRatio: 0.8
      }])
  }
}

二十、常见问题补充

1. 如何在 Vue 中操作 DOM?

html 复制代码
<template>
  <div ref="myElement">Hello</div>
</template>

<script>
export default {
  mounted() {
    // 使用 ref 操作 DOM
    this.$refs.myElement.style.color = 'red'
    
    // 原生 API
    this.$refs.myElement.addEventListener('click', this.handleClick)
  },
  beforeDestroy() {
    // 清理事件监听
    this.$refs.myElement.removeEventListener('click', this.handleClick)
  },
  methods: {
    handleClick() {
      console.log('clicked')
    }
  }
}
</script>

2. Vue 中如何避免事件冒泡?

html 复制代码
<!-- 使用 .stop 修饰符 -->
<div @click="parentHandler">
  <button @click.stop="childHandler">点击</button>
</div>

<!-- 或使用原生方式 -->
<button @click="childHandler($event)">
  <script>
    childHandler(e) {
      e.stopPropagation()
    }
  </script>
</button>

3. 如何以编程方式导航?

javascript 复制代码
// 方式一:路径字符串
this.$router.push('/home')

// 方式二:命名路由
this.$router.push({ name: 'home' })

// 方式三:带参数
this.$router.push({ name: 'user', params: { id: '123' } })

// 方式四:带查询参数
this.$router.push({ path: '/search', query: { keyword: 'vue' } })

// 替换当前记录(不留历史)
this.$router.replace('/home')

// 前进/后退
this.$router.go(-1)
this.$router.back()
this.$router.forward()

4. 如何确定组件的加载状态?

html 复制代码
<template>
  <div>
    <div v-if="isLoading">加载中...</div>
    <div v-else-if="isLoaded">加载完成</div>
    <div v-else>准备加载</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isLoading: false,
      isLoaded: false
    }
  },
  async mounted() {
    this.isLoading = true
    try {
      await this.fetchData()
      this.isLoaded = true
    } finally {
      this.isLoading = false
    }
  }
}
</script>

5. Vue 项目的热重载原理

热重载(Hot Module Replacement, HMR)通过以下方式实现:

  1. Webpack Dev Server 建立 WebSocket 连接
  2. 文件变更时,Webpack 重新编译模块
  3. 通过 WebSocket 通知浏览器
  4. 浏览器接收新模块代码,替换旧模块
  5. Vue 组件通过 module.hot API 更新
javascript 复制代码
// Vue Loader 中的 HMR 处理
if (module.hot) {
  const api = require('vue-hot-reload-api')
  api.compatible = true
  module.hot.accept()
  
  if (!module.hot.data) {
    api.createRecord(id, component)
  } else {
    api.reload(id, component)
  }
}

6. Vue 中如何捕获全局错误?

javascript 复制代码
// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue Error:', err)
  console.error('Component:', vm?.$options?.name)
  console.error('Info:', info)
  
  // 上报错误
  if (process.env.NODE_ENV === 'production') {
    reportToSentry({ error: err, component: vm?.$options?.name })
  }
}

// 全局 Promise 错误
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise Rejection:', event.reason)
})

7. Vue 中如何使用外部库(如 jQuery)?

javascript 复制代码
// 方式一:直接引入(不推荐)
import $ from 'jquery'

// 方式二:在 mounted 中初始化
export default {
  mounted() {
    $(this.$refs.el).somePlugin()
  }
}

// 方式三:封装为指令
Vue.directive('jquery-plugin', {
  inserted(el, binding) {
    $(el)[binding.value]()
  },
  unbind(el) {
    $(el).pluginDestroy()
  }
})

8. 动态绑定 Class 与 Style

html 复制代码
<template>
  <div>
    <!-- 对象语法 -->
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>
    
    <!-- 数组语法 -->
    <div :class="[activeClass, errorClass]"></div>
    
    <!-- 对象数组语法 -->
    <div :class="[{ active: isActive }, errorClass]"></div>
    
    <!-- 动态 Style -->
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    
    <!-- Style 对象 -->
    <div :style="styleObject"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isActive: true,
      hasError: false,
      activeClass: 'active',
      errorClass: 'text-danger',
      activeColor: 'red',
      fontSize: 16,
      styleObject: {
        color: 'red',
        fontSize: '13px'
      }
    }
  }
}
</script>

9. Vue 中如何合并对象?

javascript 复制代码
export default {
  methods: {
    mergeObjects() {
      // 方式一:Object.assign
      const merged = Object.assign({}, this.defaults, this.options)
      
      // 方式二:展开运算符
      const merged2 = { ...this.defaults, ...this.options }
      
      // 方式三:深拷贝合并(使用 lodash)
      const merged3 = _.merge({}, this.defaults, this.options)
    }
  }
}

10. 在 Vue 中使用 watch 侦听多个数据源

javascript 复制代码
// 方式一:分别侦听
watch: {
  firstName() { this.updateFullName() },
  lastName() { this.updateFullName() }
}

// 方式二:computed 中转
computed: {
  fullName() {
    return `${this.firstName} ${this.lastName}`
  }
},
watch: {
  fullName(newVal) {
    console.log('Full name changed:', newVal)
  }
}

// 方式三:组合值侦听
computed: {
  watchSource() {
    return `${this.firstName}-${this.lastName}`
  }
},
watch: {
  watchSource(newVal) {
    const [first, last] = newVal.split('-')
    // 处理
  }
}

11. 如何实现组件的单例模式?

javascript 复制代码
// 方式一:全局注册
import SingletonComponent from './Singleton.vue'

const SingletonPlugin = {
  install(Vue) {
    const instance = Vue.extend(SingletonComponent)
    const singleton = new instance()
    singleton.$mount()
    document.body.appendChild(singleton.$el)
    
    // 通过全局方法调用
    Vue.prototype.$showSingleton = () => {
      singleton.show()
    }
  }
}

Vue.use(SingletonPlugin)

12. 如何实现组件销毁时的资源清理?

html 复制代码
<script>
export default {
  data() {
    return {
      timer: null,
      eventHandlers: []
    }
  },
  mounted() {
    // 定时器
    this.timer = setInterval(() => {
      console.log('tick')
    }, 1000)
    
    // 事件监听
    const handler = () => { console.log('resize') }
    window.addEventListener('resize', handler)
    this.eventHandlers.push({ el: window, event: 'resize', handler })
  },
  beforeDestroy() {
    // 清理定时器
    if (this.timer) {
      clearInterval(this.timer)
    }
    
    // 清理事件监听
    this.eventHandlers.forEach(({ el, event, handler }) => {
      el.removeEventListener(event, handler)
    })
  }
}
</script>

13. 如何封装可复用组件?

html 复制代码
<!-- 通用按钮组件 Button.vue -->
<template>
  <button
    :class="['btn', `btn-${type}`, `btn-${size}`, { 'btn-block': block }]"
    :disabled="disabled"
    @click="$emit('click', $event)"
  >
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'AppButton',
  props: {
    type: {
      type: String,
      default: 'default',
      validator: v => ['default', 'primary', 'success', 'danger'].includes(v)
    },
    size: {
      type: String,
      default: 'medium',
      validator: v => ['small', 'medium', 'large'].includes(v)
    },
    disabled: Boolean,
    block: Boolean
  }
}
</script>

14. 如何编写单元测试?

javascript 复制代码
// tests/unit/HelloWorld.spec.js
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    
    expect(wrapper.text()).toMatch(msg)
  })
  
  it('emits click event when button clicked', async () => {
    const wrapper = shallowMount(HelloWorld)
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

二十一、Vue.observable

定义
Vue.observable 创建一个响应式对象,可用于简单的跨组件状态共享。

javascript 复制代码
// state.js
import Vue from 'vue'

export const state = Vue.observable({
  count: 0,
  user: null
})

export const mutations = {
  increment() {
    state.count++
  },
  setUser(user) {
    state.user = user
  }
}

// 组件中使用
import { state, mutations } from './state'

export default {
  computed: {
    count() {
      return state.count
    }
  },
  methods: {
    increment() {
      mutations.increment()
    }
  }
}

二十二、常见面试问题速查表

1. 生命周期速查

钩子 时机 可访问 常用场景
beforeCreate 实例初始化后 -
created 实例创建后 data/methods API 请求
beforeMount 挂载前 模板 最后修改数据
mounted 挂载后 DOM DOM 操作
beforeUpdate 更新前 新旧数据 更新前处理
updated 更新后 新 DOM DOM 更新后处理
beforeDestroy 销毁前 完整实例 清理资源
destroyed 销毁后 - 最终清理

2. 组件通信速查

方式 关系 方向 说明
props 父子 父→子 常用
$emit 父子 子→父 常用
$refs 父子 父→子 直接访问实例
$parent 父子 子→父 直接访问父实例
provide/inject 祖先后代 祖先→后代 跨层级
<math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s / attrs/ </math>attrs/listeners 隔代 双向 属性透传
Event Bus 任意 双向 事件总线
Vuex 任意 单向 状态管理

3. 路由守卫速查

守卫 作用范围 触发时机
beforeEach 全局 每次导航前
beforeResolve 全局 导航解析完成前
afterEach 全局 导航完成后
beforeEnter 路由 进入路由前
beforeRouteEnter 组件 进入组件前
beforeRouteUpdate 组件 路由更新时
beforeRouteLeave 组件 离开组件前

二十三、实战项目难点示例

题目:描述一个 Vue 项目开发中遇到的技术难点及解决方案

示例答案

难点一:大型表单性能问题

  • 问题:一个页面有上百个表单项,数据变化时页面明显卡顿
  • 原因:每个表单项都是响应式的,数据变化触发大量组件重新渲染
  • 解决方案
    1. 使用 Object.freeze() 冻结不需要响应式的静态数据
    2. 将大表单拆分为多个子组件,缩小更新范围
    3. 使用防抖处理输入事件
    4. 长列表使用虚拟滚动

难点二:权限控制复杂

  • 问题:不同角色看到不同的页面、菜单和按钮
  • 解决方案
    1. 路由级:使用路由守卫 + meta.roles 控制页面访问
    2. 菜单级:根据用户角色动态生成菜单
    3. 按钮级:使用自定义指令 v-permission 控制按钮显示
    4. 数据级:后端接口返回用户权限列表

难点三:首屏加载慢

  • 问题:首屏加载时间超过 5 秒
  • 解决方案
    1. 路由懒加载:() => import('./views/Home.vue')
    2. 组件按需引入:只引入使用的 Element UI 组件
    3. CDN 外部化:将 Vue、VueRouter 等通过 CDN 引入
    4. 图片压缩和懒加载
    5. 开启 gzip 压缩
    6. 最终首屏加载时间降低到 1.5 秒

相关推荐
那我掉的头发算什么1 小时前
【面试八股】一篇文章讲清楚JVM面试常考
jvm·面试·职场和发展·java虚拟机
乔代码嘚1 小时前
2026 AI大模型全套资料免费领!30天从入门到架构部署,附面试真题与行业报告
人工智能·语言模型·面试·大模型·产品经理·ai大模型·大模型学习
冬天vs不冷1 小时前
面试必知必会(13):MySQL锁机制
mysql·面试·职场和发展
华夏之光永存1 小时前
独家:国家级光刻机项目架构师面试对话实录
面试·职场和发展
冬天vs不冷1 小时前
面试必知必会(14):MySQL执行计划与SQL优化
sql·mysql·面试
草莓熊Lotso1 小时前
《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》
开发语言·c++·面试
KNeeg_1 小时前
黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐
java·redis·后端·spring·面试·职场和发展·黑马点评
FPGA小迷弟1 小时前
FPGA工程师常见面试问题,有参考答案,必学!!!
fpga开发·面试·职场和发展·verilog·fpga·modelsim
Java后端的Ai之路1 小时前
以为AI开发就是调接口?一场25K的面试让我看到真相,原来真正的技术深度在这!
人工智能·面试·职场和发展·agent·ai应用开发
「已注销」1 小时前
面试分享:二本靠7轮面试成功拿下大厂P6
前端·javascript·面试