VUE-从0开始-20260408

4 阅读45分钟

Vue

MVVM 模式

1. 什么是 Vue.js?

问题:什么是 Vue.js?

答案核心回答:Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,核心库只关注视图层,易于上手,同时也能很好地配合其他工具或第三方库实现复杂应用。

详细说明: Vue.js 由尤雨溪于 2014 年创建,是一个轻量级的 MVVM 框架。与 Angular 和 React 相比,Vue 更加轻量、灵活。它的主要特点包括:

  • 响应式数据绑定:数据变化自动更新视图
  • 组件系统:支持可复用组件
  • 指令系统:提供丰富的内置指令
  • 虚拟 DOM:提高渲染性能
  • 单文件组件:使用 .vue 文件开发组件

Vue 采用自底向上增量开发的设计,适合从小巧的项目开始,逐步扩展到复杂的大型应用。

代码示例

// Vue.js 核心概念示例
import Vue from 'vue'

// 创建一个 Vue 实例
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  template: '<div>{{ message }}</div>'
})

补充说明

  • Vue 2.x 使用 Object.defineProperty 实现响应式,Vue 3.x 使用 Proxy
  • Vue 的学习曲线平缓,文档友好,社区活跃
  • Vue CLI 提供了完整的项目脚手架

2. Vue.js 有哪些特点?

问题:Vue.js 有哪些特点?

答案核心回答:Vue.js 具有响应式数据绑定、组件化开发、虚拟 DOM、指令系统、轻量级等核心特点。

详细说明

  1. 响应式数据绑定:Vue 实现了一套响应式系统,当数据变化时,视图会自动更新,无需手动操作 DOM。
  2. 组件系统:Vue 提供强大的组件系统,支持组件的复用、嵌套和通信。
  3. 虚拟 DOM:通过虚拟 DOM 技术,最小化 DOM 操作,提高渲染性能。
  4. 指令系统:内置 v-if、v-for、v-bind、v-on 等指令,方便操作 DOM。
  5. 单文件组件:使用 .vue 文件格式,可以在一个文件中编写模板、脚本和样式。
  6. 渐进式框架:可以只使用核心库,也可以配合 Vuex、Vue Router 等生态使用。
  7. 轻量级:Vue 3.x 压缩后仅约 33KB,Gzip 模式下约 10KB。

代码示例

<!-- 单文件组件示例 -->
<template>
  <div class="component">
    <h1>{{ title }}</h1>
    <button @click="updateTitle">更新标题</button>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      title: 'Vue 特点示例'
    }
  },
  methods: {
    updateTitle() {
      this.title = '标题已更新'
    }
  }
}
</script>

<style scoped>
.component {
  padding: 20px;
}
</style>

补充说明

  • Vue 的响应式系统在首次渲染时会遍历所有数据对象
  • 使用 v-bind 和 v-on 可以实现单向和双向数据流

3. Vue.js 的核心特性有哪些?

问题:Vue.js 的核心特性有哪些?

答案核心回答:Vue.js 的核心特性包括响应式系统、组件系统、虚拟 DOM、指令系统、路由管理(Vue Router)和状态管理(Vuex/Pinia)。

详细说明

特性描述作用
响应式系统数据变化自动更新视图无需手动操作 DOM
组件系统可复用的组件提高代码复用率
虚拟 DOM内存中的 DOM 映射优化渲染性能
指令系统v-if/v-for/v-bind 等便捷操作 DOM
Vue Router官方路由管理SPA 页面跳转
Vuex/Pinia状态管理集中管理应用状态

代码示例

// Vue 响应式系统示例
const vm = new Vue({
  data: {
    user: {
      name: '张三',
      age: 25
    }
  }
})

// 数据变化时,视图自动更新
vm.user.name = '李四' // 视图中的 {{ user.name }} 会自动更新

补充说明

  • Vue 3 引入了 Composition API,提供更灵活的代码组织方式
  • Pinia 是 Vue 3 官方推荐的新一代状态管理库

4. 什么是 Vue 组件,为什么要使用组件?

问题:什么是 Vue 组件,为什么要使用组件?

答案核心回答:Vue 组件是具有独立功能、可复用的 UI 模块,使用组件可以提高代码复用率、降低维护成本、使项目结构更清晰。

详细说明: 组件是 Vue 最强大的功能之一,它可以扩展 HTML 元素,封装可重用的代码。

使用组件的好处

  1. 代码复用:相同的 UI 结构只需编写一次
  2. 易于维护:修改一个组件会影响所有使用它的地方
  3. 独立作用域:每个组件有独立的数据和方法
  4. 便于测试:组件可以独立进行单元测试
  5. 团队协作:不同团队可以并行开发不同组件

代码示例

<!-- Button.vue 组件 -->
<template>
  <button :class="['btn', `btn-${type}`]" @click="handleClick">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'ButtonComponent',
  props: {
    type: {
      type: String,
      default: 'primary'
    }
  },
  methods: {
    handleClick() {
      this.$emit('click')
    }
  }
}
</script>

<!-- 使用组件 -->
<template>
  <div>
    <ButtonComponent type="primary" @click="handleSubmit">
      提交
    </ButtonComponent>
  </div>
</template>

<script>
import ButtonComponent from './Button.vue'
export default {
  components: { ButtonComponent }
}
</script>

补充说明

  • 全局注册组件:Vue.component('button-component', ButtonComponent)
  • 局部注册组件:在组件的 components 选项中注册

5. MVVM 模式的理解

问题:MVVM 模式的理解

答案核心回答:MVVM(Model-View-ViewModel)是一种软件架构模式,Model 层处理数据,View 层展示界面,ViewModel 层连接 Model 和 View,实现双向数据绑定。

详细说明

职责Vue 中的体现
Model数据模型、业务逻辑data、computed、methods
View用户界面template、HTML
ViewModel连接 Model 和 ViewVue 实例

Vue 的响应式系统就是 MVVM 的实现:

  • Model 变化 → ViewModel 检测到变化 → View 自动更新
  • View 操作 → ViewModel 处理 → Model 更新

代码示例

// MVVM 模式示例
// Model 层
const model = {
  message: 'Hello MVVM'
}

// ViewModel 层(Vue 实例)
const viewModel = new Vue({
  el: '#app',
  data: {
    // Model
    message: 'Hello MVVM'
  },
  // View
  template: '<div>{{ message }}</div>'
})

// View 操作导致 Model 更新
viewModel.message = 'Hello Vue' // View 自动更新

补充说明

  • MVVM 模式让开发者专注于业务逻辑,无需手动操作 DOM
  • Vue 是典型的 MVVM 框架,但官方定位为"渐进式框架"

6. 解释 MVVM 模式及其在 Vue 中的体现

问题:解释 MVVM 模式及其在 Vue 中的体现

答案核心回答:MVVM 模式通过 ViewModel 实现 Model 和 View 的双向绑定,Vue 通过响应式系统、指令和模板编译完美实现了这一模式。

详细说明: Vue 中 MVVM 的具体体现:

Model 层

data: {
  title: 'Vue MVVM',
  items: [1, 2, 3]
}

View 层

<div>
  <h1>{{ title }}</h1>
  <ul>
    <li v-for="item in items">{{ item }}</li>
  </ul>
</div>

ViewModel 层

  • 响应式系统监听 Model 变化,自动更新 View
  • 指令系统处理 View 操作,更新 Model
  • 计算属性和侦听器处理业务逻辑

补充说明

  • Vue 的响应式是单向的,但 v-model 实现了双向绑定效果
  • 理解 MVVM 有助于更好地组织 Vue 应用代码

SPA 与 SSR

7. SPA 的优缺点

问题:SPA 的优缺点

答案核心回答:SPA(单页面应用)用户体验流畅,前后端分离明确,但首屏加载慢、SEO 不友好。

详细说明

优点缺点
用户体验流畅,页面切换无闪烁首屏加载时间长
前后端分离,开发效率高SEO 不友好
组件化开发,代码复用率高初次加载资源大
局部刷新,减少服务器压力前进后退操作需要自行处理
路由在前端,维护简单不支持低版本浏览器(Vue 2.x)

代码示例

// Vue Router 实现 SPA
const routes = [
  { path: '/home', component: Home },
  { path: '/about', component: About },
  { path: '/user/:id', component: User }
]

const router = new VueRouter({
  mode: 'history', // 使用 history 模式
  routes
})

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

补充说明

  • SSR 可以解决 SPA 的 SEO 和首屏加载问题
  • 路由懒加载可以优化首屏加载时间

8. 如何实现服务端渲染 (SSR)?

问题:如何实现服务端渲染 (SSR)?

答案核心回答:Vue 提供 Nuxt.js 框架和 Vue SSR 官方方案来实现服务端渲染,核心是通过 webpack 打包后,在 Node.js 服务端渲染 Vue 组件为 HTML 字符串。

详细说明

Nuxt.js 方案(推荐)

npx create-nuxt-app my-ssr-app

手动 Vue SSR 方案

  1. 安装依赖:vue-server-renderer、express
  2. 创建服务器入口
  3. 创建 webpack 配置
  4. 配置路由和 store

代码示例

// server.js - Express 服务端
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()

const app = new Vue({
  data: { message: 'Hello SSR' },
  template: '<div>{{ message }}</div>'
})

renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html) // <div data-server-rendered="true">Hello SSR</div>
})

补充说明

  • Nuxt.js 是最成熟的 Vue SSR 框架,提供了自动代码分割、路由管理等功能
  • SSR 可以显著提升首屏渲染速度和 SEO 效果

9. SSR 的实现原理

问题:SSR 的实现原理

答案核心回答:SSR 通过在 Node.js 服务端执行 Vue 代码,将组件渲染为 HTML 字符串返回给客户端,客户端再"激活"为完整的 SPA。

详细说明

SSR 流程

  1. 服务端接收请求
  2. 运行 Vue 组件代码,渲染成 HTML 字符串
  3. 返回 HTML 给浏览器显示
  4. 浏览器下载 JS 资源
  5. 客户端 hydration(激活),绑定事件和处理交互

代码示例

// 简单的 SSR 原理
// server.js
const { createApp } = require('./app')

function render(url) {
  const app = createApp()
  // 设置路由上下文
  const router = app.$router
  router.push(url)
  
  return new Promise((resolve, reject) => {
    // 等待路由组件加载完成
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // 渲染为 HTML
      resolve(app)
    })
  })
}

补充说明

  • 服务端渲染的组件不能访问 window、document 等浏览器 API
  • 需要区分服务端和客户端代码,使用 process.client 或 xxx.client.js

10. Vue 项目的 SEO 优化

问题:Vue 项目的 SEO 优化

答案核心回答:Vue SPA 项目的 SEO 优化主要包括 SSR 服务端渲染、预渲染、meta 标签管理、语义化 HTML 和 sitemap 生成。

详细说明

优化方法适用场景实现方式
SSR需要完美 SEONuxt.js
预渲染静态页面较多prerender-spa-plugin
Meta 标签社交分享vue-meta
语义化 HTML所有项目规范开发
sitemap所有项目sitemap 生成器

代码示例

// vue-meta 配置 SEO
export default {
  metaInfo: {
    title: 'Vue SEO 优化示例',
    meta: [
      { name: 'description', content: '这是一个 Vue SEO 优化示例页面' },
      { property: 'og:title', content: 'Vue SEO 优化' },
      { name: 'keywords', content: 'Vue, SEO, 优化' }
    ],
    link: [
      { rel: 'canonical', href: 'https://example.com/page' }
    ]
  }
}

补充说明

  • SPA 页面对于需要 SEO 的内容,建议使用 SSR
  • 预渲染适用于页面数量有限的静态站点

Vue CLI 与项目构建

11. Vue CLI

问题:Vue CLI

答案核心回答:Vue CLI 是 Vue.js 官方提供的命令行工具,用于快速创建和搭建 Vue 项目脚手架。

详细说明

常用命令

# 安装
npm install -g @vue/cli

# 创建项目
vue create my-project

# 启动图形界面
vue ui

# 添加插件
vue add router
vue add vuex

代码示例

# 交互式创建项目
vue create my-vue-app

# 选择配置
? Please pick a preset:
  > Default (Vue 3) ([Vue 3] babel, eslint)
  > Default (Vue 2) ([Vue 2] babel, eslint)
  > Manually select features

补充说明

  • Vue CLI 4.x 支持零配置搭建项目
  • 可以通过 vue.config.js 覆盖默认配置

12. Vue 项目构建

问题:Vue 项目构建

答案核心回答:Vue 项目通过 webpack 或 Vite 进行构建,将 .vue 文件、ES6+ 代码编译打包为浏览器可运行的静态资源。

详细说明

构建流程

  1. 安装依赖(npm install)
  2. 执行构建命令(npm run build)
  3. webpack/Vite 读取入口配置
  4. 递归解析模块依赖
  5. 编译、压缩、合并资源
  6. 输出到 dist 目录

代码示例

// vue.config.js 构建配置
module.exports = {
  outputDir: 'dist',
  assetsDir: 'static',
  productionSourceMap: false,
  devServer: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  },
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
  }
}

补充说明

  • Vue CLI 4.x 内置 webpack 4,配置更加简洁
  • 生产环境建议开启 Gzip 压缩

13. Vue 项目结构

问题:Vue 项目结构

答案核心回答:Vue 项目标准结构包括 src 源码目录(components、views、router、store、assets)、public 静态资源、配置文件等。

详细说明

my-vue-project/
├── public/              # 不经过 webpack 处理的静态资源
│   └── index.html
├── src/
│   ├── assets/          # 需要 webpack 处理的资源
│   │   ├── images/
│   │   └── styles/
│   ├── components/      # 公共组件
│   ├── views/           # 页面组件
│   ├── router/          # 路由配置
│   ├── store/           # 状态管理
│   ├── utils/           # 工具函数
│   ├── App.vue          # 根组件
│   └── main.js          # 入口文件
├── tests/               # 测试文件
├── .env.xxx             # 环境变量
├── vue.config.js       # Vue CLI 配置
└── package.json

补充说明

  • components 和 views 的区分:views 是路由页面组件
  • 可以根据项目规模调整目录结构

14. Vue 环境配置

问题:Vue 环境配置

答案核心回答:Vue 通过 .env 文件配置不同环境的环境变量,如开发、测试、生产环境,使用 process.env.xxx 访问。

详细说明

环境文件类型

文件名作用域说明
.env所有环境通用配置
.env.local所有环境本地覆盖,不提交
.env.development开发环境npm run serve 时读取
.env.production生产环境npm run build 时读取
.env.test测试环境npm run test 时读取

代码示例

# .env.development
VUE_APP_API_BASE_URL=http://localhost:3000
VUE_APP_ENV=development

# .env.production
VUE_APP_API_BASE_URL=https://api.example.com
VUE_APP_ENV=production
// 使用环境变量
const apiUrl = process.env.VUE_APP_API_BASE_URL
console.log('当前环境:', process.env.VUE_APP_ENV)

补充说明

  • 环境变量必须以 VUE_APP_ 开头才能在客户端代码中访问
  • 可以创建 .env.local 存放敏感信息(已加入 .gitignore)

Vue Router 路由

15. Vue Router 的使用

问题:Vue Router 的使用

答案核心回答:Vue Router 是 Vue.js 官方的路由管理器,用于实现 SPA 的页面跳转和导航管理。

详细说明

基本使用

npm install vue-router
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'

Vue.use(VueRouter)

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: () => import('@/views/About.vue') }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router
// main.js
import router from './router'
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

补充说明

  • 路由组件可以通过 $router 访问路由实例
  • 通过 $route 访问当前路由信息

16. 路由模式

问题:路由模式

答案核心回答:Vue Router 支持 hash 模式和 history 模式两种路由模式,hash 模式使用 URL 哈希,history 模式使用 HTML5 History API。

详细说明

模式URL 格式原理特点
hashexample.com/#/path监听 hashchange 事件无需服务器配置
historyexample.com/pathHTML5 History API需要服务器配置
abstract-支持所有环境用于非浏览器环境

代码示例

// hash 模式
const router1 = new VueRouter({
  mode: 'hash',
  routes
})

// history 模式
const router2 = new VueRouter({
  mode: 'history',
  routes
})

// history 模式需要服务器配置(所有路径都返回 index.html)
// Nginx 配置
// location / {
//   try_files $uri $uri/ /index.html;
// }

补充说明

  • 部署到生产环境时,history 模式需要服务器配置重定向到 index.html
  • 开发环境下两种模式都可以正常工作

17. hash 模式与 history 模式

问题:hash 模式与 history 模式

答案核心回答:hash 模式 URL 带 # 号,兼容性好不需要服务器配置;history 模式 URL 干净美观,但需要服务器配置支持。

详细说明

对比项hash 模式history 模式
URL 格式/#/path/path
刷新页面不需要服务器配置需要服务器配置
SEO 友好部分支持
书签支持可以可以
浏览器兼容所有浏览器IE10+

代码示例

// hash 模式示例
// URL: http://localhost:8080/#/user/123

// history 模式示例
// URL: http://localhost:8080/user/123

// 切换模式
const router = new VueRouter({
  // mode: 'hash',  // 默认
  mode: 'history',
  routes
})

补充说明

  • SPA 应用推荐使用 history 模式,用户体验更好
  • 如果是静态页面托管,推荐 hash 模式

18. 路由配置

问题:路由配置

答案核心回答:路由配置定义路径、组件、参数、守卫等映射关系,通过 routes 数组传递给 VueRouter 实例。

详细说明

// 完整路由配置示例
const routes = [
  {
    path: '/',
    redirect: '/home',  // 重定向
    component: Layout,
    children: [
      {
        path: 'home',
        name: 'Home',
        component: Home,
        meta: { title: '首页', requiresAuth: true }
      },
      {
        path: 'user/:id',  // 动态路由
        name: 'User',
        component: User,
        props: true  // 将路由 params 作为 props 传递
      }
    ]
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '*',  // 404 路由
    component: NotFound
  }
]

补充说明

  • 使用 children 配置嵌套路由
  • 使用 meta 定义自定义元信息

19. 动态路由

问题:动态路由

答案核心回答:动态路由使用路径参数(如 :id)匹配可变路径,实现如 /user/1、/user/2 这类路由。

详细说明

路由定义

{
  path: '/user/:id',        // :id 是参数
  name: 'User',
  component: User
}

访问参数

// 在组件中访问
this.$route.params.id    // 获取路由参数
this.$route.query.name  // 获取查询参数
this.$route.hash        // 获取锚点

代码示例

// 路由定义
const routes = [
  { path: '/user/:id', component: User },
  { path: '/article/:category/:id', component: Article }
]

// 使用 router-link 跳转
<router-link to="/user/123">用户123</router-link>

// 编程式导航
this.$router.push('/user/456')

// 在组件中获取参数
export default {
  created() {
    console.log(this.$route.params.id) // 123
  },
  watch: {
    $route(to, from) {
      console.log(to.params.id) // 每次路由变化都会执行
    }
  }
}

补充说明

  • 使用 props: true 可以将路由参数作为组件 props 传递
  • 路由参数变化不会触发组件的 created,需要使用 watch $route 或 beforeRouteUpdate

20. 路由守卫

问题:路由守卫

答案核心回答:路由守卫是在路由导航过程中执行的钩子函数,用于控制路由的访问权限、页面标题、滚动位置等。

详细说明

守卫类型调用时机使用场景
beforeEach路由切换前权限验证、全局拦截
beforeResolve导航确认前异步组件加载后
afterEach路由切换后页面标题、滚动

代码示例

// main.js
const router = new VueRouter({ routes })

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // to: 目标路由对象
  // from: 当前路由对象
  // next: 确认导航的函数
  
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next('/login')
  } else {
    next()  // 调用 next() 继续导航
  }
})

// 全局后置守卫
router.afterEach((to, from) => {
  document.title = to.meta.title || '默认标题'
})

补充说明

  • 记住调用 next(),否则导航会被阻止
  • next(false) 可以取消导航

21. 全局守卫

问题:全局守卫

答案核心回答:全局守卫对所有路由生效,包括 beforeEach、beforeResolve、afterEach 三种。

详细说明

beforeEach(to, from, next):每个路由切换前调用 beforeResolve(to, from, next):导航被确认前,所有组件内守卫和异步路由组件被解析后调用 afterEach(to, from):路由切换后调用,无法改变导航

代码示例

// 全局守卫示例
router.beforeEach(async (to, from, next) => {
  // 验证用户是否登录
  const hasToken = localStorage.getItem('token')
  
  if (hasToken) {
    // 已登录,允许访问
    next()
  } else {
    // 未登录,检查是否去登录页
    if (to.path === '/login') {
      next()
    } else {
      next('/login')
    }
  }
})

// 验证权限示例
router.beforeEach(async (to, from, next) => {
  const requiredRoles = to.meta.roles || []
  const userRole = getUserRole()
  
  if (requiredRoles.includes(userRole)) {
    next()
  } else {
    next('/403')
  }
})

补充说明

  • 守卫是异步解析的,执行顺序很重要
  • 可以使用 store 管理认证状态

22. 路由独享守卫

问题:路由独享守卫

答案核心回答:路由独享守卫是在路由配置中定义的守卫,只在进入该路由时触发,不会在路由参数变化时触发。

详细说明

beforeEnter(to, from, next):路由配置中定义,进入路由前调用

代码示例

const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      // 只有进入 /admin 时触发
      const isAdmin = localStorage.getItem('role') === 'admin'
      if (isAdmin) {
        next()
      } else {
        next('/403')
      }
    }
  },
  {
    path: '/user/:id',
    component: User,
    beforeEnter: (to, from, next) => {
      // 首次进入 /user/1 触发
      // 但从 /user/1 切换到 /user/2 不触发
      next()
    }
  }
]

补充说明

  • beforeEnter 不会在 params 变化时触发(如 /user/1 到 /user/2)
  • 如果需要监听参数变化,在组件内使用 beforeRouteUpdate

23. 组件内守卫

问题:组件内守卫

答案核心回答:组件内守卫是定义在组件内部的路由守卫,包括 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。

详细说明

守卫调用时机
beforeRouteEnter渲染该组件的路由确认前调用
beforeRouteUpdate当前路由改变,但该组件被复用时调用
beforeRouteLeave导航离开该组件时调用

代码示例

export default {
  name: 'UserProfile',
  data() {
    return { user: null }
  },
  // 路由进入前 - 不能访问 this
  beforeRouteEnter(to, from, next) {
    // 可以访问 vm 实例
    next(vm => {
      vm.fetchUser(to.params.id)
    })
  },
  // 路由参数变化时(复用组件)
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.id)
    next()
  },
  // 离开确认
  beforeRouteLeave(to, from, next) {
    const hasUnsavedChanges = this.hasUnsavedChanges
    if (hasUnsavedChanges) {
      const answer = confirm('有未保存的更改,确定离开吗?')
      if (answer) next()
      else next(false)
    } else {
      next()
    }
  }
}

补充说明

  • beforeRouteEnter 是唯一可以访问 this 的守卫(通过 next 的回调)
  • 可以使用导航守卫的 to.meta 传递信息

24. 路由懒加载

问题:路由懒加载

答案核心回答:路由懒加载通过动态导入(import())实现组件的按需加载,减少首屏加载资源体积。

详细说明

懒加载写法

// 方式1:动态导入(推荐)
const Home = () => import('@/views/Home.vue')

// 方式2:带命名 chunk 的动态导入
const User = () => import(/* webpackChunkName: "user" */ '@/views/User.vue')

// 方式3:Webpack 魔法注释
const About = () => import(/* webpackPrefetch: true */ '@/views/About.vue')

代码示例

// router/index.js
const routes = [
  {
    path: '/',
    // 非懒加载 - 所有组件一起打包
    component: Home
  },
  {
    path: '/about',
    // 懒加载 - 单独打包成一个 chunk
    component: () => import('@/views/About.vue')
  },
  {
    path: '/user',
    // 路由级别的代码分割
    children: [
      {
        path: 'list',
        component: () => import('@/views/user/List.vue')
      },
      {
        path: 'detail/:id',
        component: () => import('@/views/user/Detail.vue')
      }
    ]
  }
]

// 使用 WebpackChunkName 合并多个路由到同一 chunk
const UserList = () => import(/* webpackChunkName: "user" */ '@/views/user/List.vue')
const UserEdit = () => import(/* webpackChunkName: "user" */ '@/views/user/Edit.vue')

补充说明

  • prefetch 会在空闲时预取资源
  • suspense 配合 lazy loading 可以显示加载状态

25. routerrouter 与 route 的区别

问题routerrouter 与 route 的区别

答案核心回答router是路由实例,包含路由操作方法(pushreplace等);router 是路由实例,包含路由操作方法(push、replace 等);route 是当前路由对象,包含路由信息(params、query、meta 等)。

详细说明

对象类型用途
$routerVueRouter 实例编程式导航、操作路由
$route路由对象访问当前路由信息

代码示例

// $router - 路由实例,提供操作方法
this.$router.push('/home')           // 导航到 home
this.$router.replace('/about')       // 替换当前路由
this.$router.go(-1)                  // 前进/后退
this.$router.back()                  // 后退
this.$router.forward()               // 前进

// $route - 当前路由对象,提供路由信息
console.log(this.$route.path)        // /user/123
console.log(this.$route.params)      // { id: '123' }
console.log(this.$route.query)       // { name: 'zs' }
console.log(this.$route.meta)        // { title: '用户详情' }
console.log(this.$route.name)       // User
console.log(this.$route.hash)        // #section

补充说明

  • router是全局唯一的,router 是全局唯一的,route 是当前活跃的路由
  • 在模板中使用时不需要加 this

Vue 原理

26. Vue 依赖收集

问题:Vue 依赖收集

答案核心回答:Vue 通过数据劫持和订阅者模式实现依赖收集,当数据变化时自动通知所有依赖进行更新。

详细说明

原理流程

  1. 在 render 函数中访问数据时,触发 getter
  2. getter 中将当前渲染 watcher 添加到数据的依赖列表
  3. 数据变化时,触发 setter,通知所有依赖的 watcher 更新

代码示例

// 简化的依赖收集实现
class Dep {
  constructor() {
    this.subscribers = new Set()  // 存储依赖的 watcher
  }
  
  depend() {
    if (activeWatcher) {
      this.subscribers.add(activeWatcher)
    }
  }
  
  notify() {
    this.subscribers.forEach(watcher => watcher.update())
  }
}

// 在 getter 中收集依赖
function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    const dep = new Dep()
    
    Object.defineProperty(obj, key, {
      get() {
        dep.depend()  // 收集依赖
        return value
      },
      set(newValue) {
        value = newValue
        dep.notify()  // 通知更新
      }
    })
  })
}

// 使用
const data = reactive({ name: '张三' })

// 当 render 函数访问 data.name 时,自动收集依赖

补充说明

  • Vue 2.x 使用 Object.defineProperty,Vue 3.x 使用 Proxy
  • 一个数据可以有多个依赖(多个组件使用同一数据)

27. Vue 派发更新

问题:Vue 派发更新

答案核心回答:派发更新是当响应式数据变化时,Vue 通过 Dep 通知所有订阅的 Watcher,触发异步更新队列。

详细说明

更新流程

  1. 数据变化触发 setter
  2. setter 调用 dep.notify()
  3. 所有订阅的 watcher 被放入更新队列
  4. 通过 nextTick 异步执行所有 watcher.update()
  5. 触发组件重新渲染

代码示例

// 派发更新实现
class Dep {
  constructor() {
    this.subscribers = []
  }
  
  addSub(watcher) {
    this.subscribers.push(watcher)
  }
  
  notify() {
    // 通知所有订阅者
    this.subscribers.forEach(watcher => watcher.update())
  }
}

class Watcher {
  constructor(vm, exp, callback) {
    this.vm = vm
    this.exp = exp
    this.callback = callback
  }
  
  update() {
    // 派发更新时,触发回调
    this.callback()
  }
}

// 数据变化时
function setData(key, value) {
  data[key] = value  // 触发 setter
  // setter 中的 notify() 会派发更新
}

补充说明

  • Vue 使用异步更新队列避免重复渲染
  • 同一个 tick 中多次修改同一数据,只会触发一次渲染

28. Vue 异步更新队列

问题:Vue 异步更新队列

答案核心回答:Vue 将同一事件循环中的多次数据变化合并为一次 DOM 更新,通过 nextTick 在下一个 tick 中执行。

详细说明

异步更新原理

  1. 响应式数据变化时,触发 watcher 更新
  2. watcher 更新被放入队列(queueWatcher)
  3. 队列去重,避免重复更新
  4. nextTick 时统一执行队列中的更新
  5. 执行完成后,触发组件重新渲染

代码示例

// 异步更新示例
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    async increment() {
      this.count++  // 触发更新,但不会立即渲染
      this.count++  // 第二次变化,会被合并
      this.count++  // 第三次变化,会被合并
      
      // DOM 不会立即更新
      console.log(this.$refs.counter.textContent) // 0
      
      // 使用 nextTick 获取更新后的 DOM
      await this.$nextTick()
      console.log(this.$refs.counter.textContent) // 3
    }
  }
}

补充说明

  • nextTick 可以传入回调函数或返回 Promise
  • 内部使用 Promise、MutationObserver 或 setTimeout 实现

29. Vue diff 算法

问题:Vue diff 算法

答案核心回答:Vue 的 diff 算法采用虚拟 DOM 的差异化比较策略,通过同层比较和 key 优化,实现高效的 DOM 更新。

详细说明

diff 策略

  1. 同层比较:只比较同一层级的节点,不跨层比较
  2. 先比较标签:标签不同则直接替换
  3. 同标签比较:比较属性和子节点
  4. 使用 key:key 帮助识别节点,提高复用率

代码示例

// diff 算法的简化实现
function updateChildren(oldChildren, newChildren) {
  let oldStartIndex = 0
  let newStartIndex = 0
  let oldEndIndex = oldChildren.length - 1
  let newEndIndex = newChildren.length - 1
  
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 双端比较
    const oldStartVnode = oldChildren[oldStartIndex]
    const newStartVnode = newChildren[newStartIndex]
    
    if (sameVnode(oldStartVnode, newStartVnode)) {
      // 递归比较子节点
      patch(oldStartVnode, newStartVnode)
      oldStartIndex++
      newStartIndex++
    } else {
      // 移动指针继续比较
      // ...
    }
  }
}

// 判断是否为相同节点
function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag
}

补充说明

  • 使用唯一的 key 可以提高 diff 性能
  • 避免使用 index 作为 key,可能导致状态错乱

30. Vue 模板编译

问题:Vue 模板编译

答案核心回答:Vue 模板编译将 .vue 文件中的 template 编译为 render 函数,经历解析(parse)、优化(optimize)、生成(generate)三个阶段。

详细说明

编译三阶段

阶段输出说明
parseAST 抽象语法树解析模板字符串为 AST
optimize优化后的 AST标记静态节点,跳过 diff
generaterender 函数生成可执行代码字符串

代码示例

// 模板编译示例
const template = `<div class="container">
  <h1>{{ title }}</h1>
  <p>{{ content }}</p>
</div>`

// 编译后的大致 render 函数
function render() {
  return createElement('div', {
    attrs: { class: 'container' }
  }, [
    createElement('h1', {}, [toDisplayString(this.title)]),
    createElement('p', {}, [toDisplayString(this.content)])
  ])
}

// 使用 vue-template-compiler
const compiler = require('vue-template-compiler')
const result = compiler.compile(template)

console.log(result.render)    // render 函数代码
console.log(result.staticRenderFns)  // 静态渲染函数

补充说明

  • Vue CLI 构建的项目会提前编译模板
  • runtime-only 版本不支持 template,需要 render 函数

31. Vue 渲染过程

问题:Vue 渲染过程

答案核心回答:Vue 渲染过程是模板编译 → 生成 render 函数 → 创建虚拟 DOM → 映射为真实 DOM 的过程。

详细说明

渲染流程

模板(template)
    ↓ 编译
render 函数
    ↓ 执行
虚拟 DOM (vnode)
    ↓ patch
真实 DOM

代码示例

// Vue 渲染过程详解
new Vue({
  el: '#app',
  data: { message: 'Hello' },
  template: '<div>{{ message }}</div>'
  // 编译流程:
  // 1. 将 template 编译成 render 函数
  // 2. 执行 render 函数,创建 vnode
  // 3. 通过 patch(vnode) 创建真实 DOM
  // 4. 挂载到 #app
})

// render 函数形式
new Vue({
  data: { message: 'Hello' },
  render(h) {
    return h('div', this.message)
  }
})

// 组件更新流程
// 数据变化 → setter 通知 Watcher → 重新执行 render → diff 比较 → 更新 DOM

补充说明

  • 组件可以只提供 render 函数,不使用 template
  • 使用 render 函数可以更灵活地控制渲染逻辑

Vue 实例与生命周期

32. Vue 生命周期有哪些?

问题:Vue 生命周期有哪些?

答案核心回答:Vue 2.x 生命周期包括创建、挂载、更新、销毁四个阶段共 11 个钩子函数。

详细说明

生命周期图示

beforeCreate → created → beforeMount → mounted 
→ beforeUpdate → updated → beforeDestroy → destroyed

详细列表

阶段钩子函数说明
创建beforeCreate实例初始化后,数据观测之前
创建created实例创建完成,属性已绑定
挂载beforeMount模板编译完成,即将挂载
挂载mounted实例挂载到 DOM
更新beforeUpdate数据变化,DOM 更新前
更新updatedDOM 更新完成
销毁beforeDestroy实例销毁前,清理定时器等
销毁destroyed实例销毁后

补充说明

  • Vue 3.x 生命周期有所变化(setup 替代 beforeCreate/created)
  • destroyed 改名为 unmounted

33. Vue 的生命周期钩子函数

问题:Vue 的生命周期钩子函数

答案核心回答:生命周期钩子函数是在 Vue 实例从创建到销毁过程中的不同阶段自动执行的回调函数。

详细说明

代码示例

new Vue({
  data: { message: 'Hello' },
  
  // 实例初始化后,数据观测之前
  beforeCreate() {
    console.log('beforeCreate:', this.message) // undefined
  },
  
  // 实例创建完成,属性已绑定
  created() {
    console.log('created:', this.message) // 'Hello'
    // 可以访问 data、computed、methods
    // 尚未挂载到 DOM
  },
  
  // 模板编译完成,即将挂载
  beforeMount() {
    console.log('beforeMount:', this.$el) // undefined
  },
  
  // 实例挂载到 DOM
  mounted() {
    console.log('mounted:', this.$el) // <div>...</div>
    // 可以操作 DOM
  },
  
  // 数据变化,DOM 更新前
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  
  // DOM 更新完成
  updated() {
    console.log('updated')
  },
  
  // 实例销毁前
  beforeDestroy() {
    // 清理定时器、事件监听器
  },
  
  // 实例销毁后
  destroyed() {
    // 销毁完成
  }
})

补充说明

  • 钩子函数的 this 指向 Vue 实例
  • 不要在钩子中使用箭头函数,会导致 this 丢失

34. Vue 生命周期的作用

问题:Vue 生命周期的作用

答案核心回答:生命周期钩子让开发者可以在特定阶段执行自定义逻辑,如初始化数据、发送请求、操作 DOM、清理资源等。

详细说明

钩子函数典型用途
created初始化非响应式数据、发送初始请求
mounted操作 DOM、添加事件监听
updatedDOM 操作、状态同步
beforeDestroy清理定时器、取消订阅、解绑事件

代码示例

export default {
  data() {
    return {
      users: [],
      timer: null
    }
  },
  
  created() {
    // 发送 API 请求
    this.fetchUsers()
    
    // 初始化非响应式数据
    this.initSomeData()
  },
  
  mounted() {
    // 操作 DOM
    this.$refs.title.textContent = 'Loaded'
    
    // 添加事件监听
    window.addEventListener('resize', this.handleResize)
    
    // 启动定时器
    this.timer = setInterval(() => this.pollData(), 5000)
  },
  
  beforeDestroy() {
    // 清理工作
    clearInterval(this.timer)
    window.removeEventListener('resize', this.handleResize)
    
    // 取消订阅
    this.subscription.unsubscribe()
  }
}

补充说明

  • 善用生命周期可以避免内存泄漏
  • created 适合做数据初始化,比 mounted 更早执行

35. created 与 mounted 的区别

问题:created 与 mounted 的区别

答案核心回答:created 在实例创建完成后立即调用,此时 DOM 不可操作;mounted 在实例挂载到 DOM 后调用,可以操作 DOM。

详细说明

对比项createdmounted
执行时机实例创建完成,数据已绑定实例挂载到 DOM
DOM 操作不可用 $refs可以使用 $refs
父组件父组件 created 后调用父组件 mounted 后调用
适用场景初始化数据、发送请求DOM 操作、第三方库初始化

代码示例

export default {
  data() {
    return { message: '' }
  },
  
  created() {
    console.log('created:', this.message) // 可以访问
    console.log('created:', this.$refs.myDiv) // undefined
    
    // 适合:发送请求、初始化数据
    this.fetchData()
  },
  
  mounted() {
    console.log('mounted:', this.$refs.myDiv) // <div>...</div>
    
    // 适合:DOM 操作、图表初始化
    this.initChart()
    this.$refs.myDiv.style.color = 'red'
  }
}

补充说明

  • 如果需要操作 DOM,使用 mounted
  • SSR 时 created 会被调用,mounted 不会

36. Vue 实例的生命周期

问题:Vue 实例的生命周期

答案核心回答:Vue 实例从创建、运行到销毁的过程,期间会触发一系列生命周期钩子,让开发者有机会在各个阶段执行逻辑。

详细说明

完整生命周期

  1. new Vue() - 创建实例
  2. beforeCreate - 初始化注入和响应式
  3. created - 实例创建完成
  4. 编译模板 - 挂载前准备
  5. beforeMount - 挂载前
  6. mounted - 挂载到 DOM
  7. 运行中 - 数据变化时
  8. beforeUpdate - 更新前
  9. updated - 更新后
  10. 销毁 - 组件卸载时
  11. beforeDestroy - 销毁前
  12. destroyed - 销毁后

代码示例

// 完整的生命周期示例
const app = new Vue({
  el: '#app',
  beforeCreate() {
    console.log('beforeCreate - 实例开始初始化')
  },
  created() {
    console.log('created - 实例创建完成,DOM 不可用')
  },
  beforeMount() {
    console.log('beforeMount - 模板编译完成,即将挂载')
  },
  mounted() {
    console.log('mounted - 实例已挂载到 DOM')
    
    // 触发更新
    this.message = 'updated'
  },
  beforeUpdate() {
    console.log('beforeUpdate - 数据变化,即将更新 DOM')
  },
  updated() {
    console.log('updated - DOM 更新完成')
  },
  beforeDestroy() {
    console.log('beforeDestroy - 实例即将销毁')
  },
  destroyed() {
    console.log('destroyed - 实例已销毁')
  }
})

补充说明

  • 理解生命周期对于调试和排查问题很重要
  • 可以使用 Vue DevTools 查看组件状态

37. Vue 生命周期的理解

问题:Vue 生命周期的理解

答案核心回答:Vue 生命周期是实例从创建到销毁的完整过程,通过钩子函数让开发者在不同阶段介入,控制应用的初始化、渲染、更新和清理逻辑。

详细说明

为什么需要生命周期

  • 不同阶段需要执行不同操作
  • 避免在错误的阶段做错误的操作
  • 让代码更加清晰和可控

代码示例

// 生命周期阶段与操作对应关系
export default {
  // 创建阶段 - 准备数据
  created() {
    this.fetchInitialData()
  },
  
  // 挂载阶段 - 操作 DOM
  mounted() {
    this.initThirdPartyLib()
  },
  
  // 更新阶段 - 响应变化
  updated() {
    this.adjustLayout()
  },
  
  // 销毁阶段 - 清理资源
  beforeDestroy() {
    this.cleanup()
  }
}

补充说明

  • Vue 3 的 Composition API 中使用 onMounted、onUpdated 等函数
  • 生命周期是 Vue 响应式系统的重要组成部分

38. Vue 生命周期各个阶段

问题:Vue 生命周期各个阶段

答案核心回答:Vue 生命周期分为创建、挂载、更新、销毁四个主要阶段,每个阶段有对应的钩子函数。

详细说明

阶段钩子可做的事
创建前beforeCreate实例初始化
创建后created访问 data、methods
挂载前beforeMount最后准备
挂载后mounted访问 DOM
更新前beforeUpdate获取更新前状态
更新后updated获取更新后状态
销毁前beforeDestroy清理定时器等
销毁后destroyed清理完成

代码示例

export default {
  name: 'LifecycleDemo',
  beforeCreate() {
    console.log('1. beforeCreate - 组件实例刚创建')
  },
  created() {
    console.log('2. created - 组件数据已绑定')
  },
  beforeMount() {
    console.log('3. beforeMount - 模板即将编译')
  },
  mounted() {
    console.log('4. mounted - DOM 已挂载')
  },
  beforeUpdate() {
    console.log('5. beforeUpdate - 数据更新,DOM 即将重新渲染')
  },
  updated() {
    console.log('6. updated - DOM 已更新')
  },
  beforeDestroy() {
    console.log('7. beforeDestroy - 组件即将销毁')
  },
  destroyed() {
    console.log('8. destroyed - 组件已销毁')
  }
}

补充说明

  • keep-alive 会缓存组件,触发 activated/deactivated 而不是 created/mounted

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

问题:Vue 父子组件生命周期执行顺序

答案核心回答:创建时父组件先创建,销毁时子组件先销毁;挂载顺序是父created→子created→子mounted→父mounted。

详细说明

创建阶段(从外到内)

父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted

更新阶段

父更新 → 父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated

销毁阶段(从内到外)

父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed

代码示例

// Parent.vue
export default {
  name: 'Parent',
  created() {
    console.log('父 created')
  },
  mounted() {
    console.log('父 mounted')
  },
  beforeDestroy() {
    console.log('父 beforeDestroy')
  }
}

// Child.vue
export default {
  name: 'Child',
  created() {
    console.log('子 created')
  },
  mounted() {
    console.log('子 mounted')
  },
  beforeDestroy() {
    console.log('子 beforeDestroy')
  }
}

// 输出顺序:
// 父 created → 子 created → 子 mounted → 父 mounted

补充说明

  • 兄弟组件按引入顺序执行
  • 异步组件会在挂载完成后才渲染

40. Vue 组件生命周期执行顺序

问题:Vue 组件生命周期执行顺序

答案核心回答:组件生命周期执行顺序遵循"父创建子创建、父挂载子挂载、子销毁父销毁"的原则,嵌套组件按深度优先遍历执行。

详细说明

完整挂载顺序

Root beforeCreate
Root created
Root beforeMount
  Child1 beforeCreate
  Child1 created
  Child1 beforeMount
  Child1 mounted
Root mounted

销毁顺序

Root beforeDestroy
  Child1 beforeDestroy
  Child1 destroyed
Root destroyed

代码示例

<!-- App.vue -->
<template>
  <div>
    <h1>父组件</h1>
    <ChildComponent />
  </div>
</template>

<!-- 输出:
App beforeCreate
App created
App beforeMount
ChildComponent beforeCreate
ChildComponent created
ChildComponent beforeMount
ChildComponent mounted
App mounted
-->

补充说明

  • Vue 3 中使用了 setup() 替代部分生命周期钩子
  • 使用 keep-alive 时,被缓存组件触发 activated/deactivated

Vue 性能优化

41. Vue 长列表优化

问题:Vue 长列表优化

答案核心回答:长列表优化主要使用虚拟滚动技术,只渲染可视区域内的列表项,典型方案有 vue-virtual-scroller 和 vue-virtual-scroll-list。

详细说明

优化策略

方法适用场景实现方式
虚拟滚动数据量大的列表只渲染可视区域
分页加载数据量中等手动加载更多
懒加载图片列表滚动到可视区再加载
Object.freeze不变数据冻结对象禁用响应式

代码示例

// 1. 使用 frozen 处理不变数据
export default {
  data() {
    return {
      // 大数据列表,不需要响应式
      list: Object.freeze(largeArray)
    }
  }
}

// 2. 分页加载
export default {
  data() {
    return {
      page: 1,
      pageSize: 20,
      list: []
    }
  },
  methods: {
    async loadMore() {
      const newItems = await fetchList(this.page, this.pageSize)
      this.list.push(...newItems)
      this.page++
    }
  }
}

// 3. 使用 vue-virtual-scroller
import RecycleScroller from 'vue-virtual-scroller'
export default {
  components: { RecycleScroller },
  template: `
    <RecycleScroller :items="largeList" :item-size="50">
      <template #default="{ item }">
        <div class="item">{{ item.name }}</div>
      </template>
    </RecycleScroller>
  `
}

补充说明

  • 虚拟滚动适合数据量上千行的场景
  • 简单列表可以使用 computed + filter 实现分页

42. Vue 首屏优化

问题:Vue 首屏优化

答案核心回答:Vue 首屏优化包括路由懒加载、图片压缩、骨架屏、SSR、代码分割等手段减少首屏加载时间。

详细说明

优化方法效果实现难度
路由懒加载减少主包体积
组件懒加载按需加载组件
图片压缩减少资源大小
骨架屏提升感知速度
SSR首屏直出
Gzip压缩传输
CDN加速资源加载

代码示例

// 1. 路由懒加载
const Home = () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')

// 2. 组件按需加载
const BigComponent = () => import('@/components/BigComponent.vue')

// 3. 使用骨架屏
// App.vue
<template>
  <div id="app">
    <Skeleton v-if="loading" />
    <router-view v-else />
  </div>
</template>

// 4. preload 预加载关键资源
// index.html
<link rel="preload" href="/src/main.js" as="script">

补充说明

  • Chrome DevTools 的 Lighthouse 可以测试首屏性能
  • 优化要适度,避免过度优化

43. Vue 打包优化

问题:Vue 打包优化

答案核心回答:Vue 打包优化包括代码分割、Tree Shaking、Gzip 压缩、CDN 加速、依赖优化等策略。

详细说明

优化策略

方法配置方式效果
代码分割import() 懒加载减少主包体积
Tree Shakingproduction 模式移除未使用代码
GzipconfigureWebpack减少传输体积
外部依赖externals不打入 bundle
Source MapproductionSourceMap减小体积

代码示例

// vue.config.js 打包优化配置
module.exports = {
  productionSourceMap: false,  // 关闭 Source Map
  
  configureWebpack: {
    externals: {
      // 第三方库通过 CDN 引入
      vue: 'Vue',
      vuex: 'Vuex',
      'vue-router': 'VueRouter'
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            priority: 10
          }
        }
      }
    }
  }
}
<!-- index.html CDN 引入 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>

补充说明

  • 使用 analyze 插件分析打包体积
  • 合理使用动态 import() 实现路由和组件懒加载

Vuex 状态管理

44. Vuex

问题:Vuex

答案核心回答:Vuex 是 Vue.js 的官方状态管理库,采用集中式存储管理应用所有组件的状态,以响应式方式实现状态共享。

详细说明

为什么需要 Vuex

  • 组件层级深,props 层层传递繁琐
  • 兄弟组件共享状态困难
  • 状态变化难以追踪调试

Vuex 核心概念

  • State:存储状态数据
  • Getters:类似 computed 计算属性
  • Mutations:同步修改状态
  • Actions:异步操作,提交 mutations
  • Modules:模块化管理状态

补充说明

  • Vue 3 推荐使用 Pinia 作为新一代状态管理
  • Vuex 适合中大型复杂应用

45. Vuex 的核心概念

问题:Vuex 的核心概念

答案核心回答:Vuex 核心概念包括 State(状态)、Getter(计算属性)、Mutation(同步修改)、Action(异步操作)、Module(模块化)。

详细说明

概念职责特点
State存储状态响应式
Getter计算属性缓存计算结果
Mutation同步修改状态必须同步
Action异步操作提交 mutation
Module模块化命名空间隔离

代码示例

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    user: null
  },
  
  getters: {
    doubleCount: state => state.count * 2,
    isLoggedIn: state => !!state.user
  },
  
  mutations: {
    INCREMENT(state) {
      state.count++
    },
    SET_USER(state, user) {
      state.user = user
    }
  },
  
  actions: {
    async login({ commit }, credentials) {
      const user = await api.login(credentials)
      commit('SET_USER', user)
    }
  },
  
  modules: {
    // 模块化
  }
})

补充说明

  • 严格模式下,mutation 外修改状态会报错
  • devtools 可以追踪 mutation 历史

46. Vuex 的使用

问题:Vuex 的使用

答案核心回答:Vuex 使用流程是创建 store、在 Vue 实例中注册、在组件中通过 $store 访问状态或提交变更。

详细说明

使用步骤

  1. 安装 Vuex:npm install vuex
  2. 创建 store 文件
  3. 在 main.js 中注册
  4. 在组件中访问状态

代码示例

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

// main.js
import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

// 组件中使用
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

补充说明

  • 可以使用 mapState、mapMutations 等辅助函数简化代码
  • 严格模式下建议开启

47. State

问题:State

答案核心回答:State 是 Vuex 中的唯一数据源,定义应用的状态树,通过 this.$store.state.xxx 访问。

详细说明

State 特点

  • 单一状态树,整个应用只有一份 store
  • 响应式,数据变化会自动更新视图
  • 不能直接修改,必须通过 mutation

代码示例

// 定义 State
const store = new Vuex.Store({
  state: {
    user: {
      name: '张三',
      age: 25
    },
    todos: [
      { id: 1, text: '学习 Vue', done: true },
      { id: 2, text: '学习 Vuex', done: false }
    ],
    loading: false
  }
})

// 组件中访问
export default {
  computed: {
    user() {
      return this.$store.state.user
    },
    todos() {
      return this.$store.state.todos
    },
    // 使用 mapState 辅助函数
    ...mapState(['loading', 'user'])
  }
}

补充说明

  • 组件中应避免直接修改 state
  • 使用模块化的 Module 可以让 state 更有组织

48. Mutation

问题:Mutation

答案核心回答:Mutation 是 Vuex 中修改状态的方法,必须是同步函数,通过 store.commit() 调用。

详细说明

Mutation 规则

  • 必须同步操作
  • 必须通过 commit 调用
  • 接收 state 作为第一个参数
  • 可以接收额外参数(payload)

代码示例

// 定义 Mutation
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    // 无参数
    increment(state) {
      state.count++
    },
    // 带 payload
    incrementBy(state, payload) {
      state.count += payload.amount
    },
    // 更新对象
    setUser(state, user) {
      state.user = { ...state.user, ...user }
    }
  }
})

// 调用 Mutation
methods: {
  increment() {
    this.$store.commit('increment')
  },
  incrementBy(amount) {
    this.$store.commit('incrementBy', { amount })
  },
  // 使用 mapMutations
  ...mapMutations(['increment', 'incrementBy'])
}

补充说明

  • devtools 可以追踪每次 mutation
  • 常量替代 mutation 名称可以避免拼写错误

49. Action

问题:Action

答案核心回答:Action 是 Vuex 中用于处理异步操作和提交 mutation 的函数,通过 store.dispatch() 调用。

详细说明

Action vs Mutation

对比项ActionMutation
异步操作支持不支持
调用方式dispatchcommit
参数context + payloadstate + payload
业务逻辑适合不适合

代码示例

const store = new Vuex.Store({
  state: {
    user: null
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user
    }
  },
  actions: {
    // 登录是异步操作
    async login({ commit }, credentials) {
      try {
        const user = await api.login(credentials)
        commit('SET_USER', user)
        return user
      } catch (error) {
        console.error('登录失败:', error)
        throw error
      }
    },
    
    // 组合多个 action
    async loadUserData({ dispatch }) {
      await dispatch('login')  // 等待登录完成
      await dispatch('fetchProfile')
    }
  }
})

// 调用 Action
methods: {
  async handleLogin() {
    const user = await this.$store.dispatch('login', this.credentials)
    console.log('登录成功:', user)
  }
}

补充说明

  • Action 可以包含任意异步操作
  • 返回 Promise 可以追踪异步操作状态

50. Vuex 的数据流

问题:Vuex 的数据流

答案核心回答:Vuex 数据流是单向的:View 通过 dispatch 调用 Action,Action 执行异步操作后 commit Mutation,Mutation 修改 State,State 变化后响应式更新 View。

详细说明

数据流图示

View (组件)
    ↓ dispatch
Action (异步)
    ↓ commit
Mutation (同步)
    ↓ mutate
State (响应式)
    ↓ reactive
View (组件)

代码示例

// 完整数据流示例
// 1. View 触发 Action
export default {
  methods: {
    async addTodo() {
      // dispatch action
      await this.$store.dispatch('addTodo', { text: this.inputText })
    }
  }
}

// 2. Action 执行异步操作后提交 Mutation
actions: {
  async addTodo({ commit }, todo) {
    const newTodo = await api.createTodo(todo)
    // 3. 提交 Mutation
    commit('ADD_TODO', newTodo)
  }
}

// 4. Mutation 修改 State
mutations: {
  ADD_TODO(state, todo) {
    state.todos.push(todo)
  }
}

// 5. State 变化,View 自动更新
computed: {
  todos() {
    return this.$store.state.todos
  }
}

补充说明

  • 严格模式下,mutation 外修改状态会报错
  • 遵循数据流可以更好地追踪状态变化

其他 Vue 特性

51. keep-alive 的使用

问题:keep-alive 的使用

答案核心回答:keep-alive 是 Vue 内置的抽象组件,用于缓存动态组件,避免重复创建和销毁,保持组件状态。

详细说明

keep-alive 特点

  • 不会渲染为真实 DOM
  • 包裹的组件会被缓存
  • 激活时触发 activated,停用时触发 deactivated
  • 可以使用 include/exclude 控制缓存

代码示例

<!-- 基础用法 -->
<keep-alive>
  <component :is="currentComponent" />
</keep-alive>

<!-- 缓存多个组件 -->
<keep-alive include="Home,About">
  <router-view />
</keep-alive>

<!-- 使用条件 -->
<keep-alive :include="['Home', 'Profile']" :exclude="['Login']" :max="10">
  <router-view />
</keep-alive>

<!-- 组件中使用 -->
export default {
  activated() {
    // 组件被激活时调用
    console.log('组件被激活')
  },
  deactivated() {
    // 组件被停用时调用
    console.log('组件被停用')
  }
}

补充说明

  • 适合 Tab 切换、列表详情等场景
  • 可以使用 LRU 算法限制缓存数量

52. Vue 插件

问题:Vue 插件

答案核心回答:Vue 插件是一个包含 install 方法的对象或函数,通过 Vue.use() 全局安装,扩展 Vue 功能。

详细说明

插件类型

类型示例
全局方法vue-router
全局资源vue.directive
组件Element UI
混入Vue.mixin

代码示例

// 创建一个插件
const myPlugin = {
  install(Vue, options) {
    // 1. 添加全局方法或属性
    Vue.myGlobalMethod = function() {}
    Vue.myGlobalProperty = 'test'
    
    // 2. 添加全局指令
    Vue.directive('focus', {
      inserted(el) {
        el.focus()
      }
    })
    
    // 3. 注入组件选项
    Vue.mixin({
      created() {
        console.log('全局混入')
      }
    })
    
    // 4. 添加实例方法
    Vue.prototype.$myMethod = function() {}
  }
}

// 使用插件
import Vue from 'vue'
import myPlugin from './myPlugin'

Vue.use(myPlugin, { /* 选项 */ })

补充说明

  • 插件的 install 方法接收 Vue 构造器和选项
  • Vue.use 会阻止重复安装插件

53. Vue.extend

问题:Vue.extend

答案核心回答:Vue.extend 用于创建 Vue 构造器的子类,返回一个可以创建 Vue 实例的子类构造器。

详细说明

使用场景

  • 创建可复用的组件构造器
  • 动态创建组件实例
  • 实现自定义组件库

代码示例

// 创建组件构造器
const Profile = Vue.extend({
  template: '<div>{{ name }} - {{ age }}</div>',
  data() {
    return {
      name: '张三',
      age: 25
    }
  }
})

// 创建实例
const profile = new Profile()
profile.$mount()
document.body.appendChild(profile.$el)

// 动态创建带 props 的组件
const DynamicComponent = Vue.extend(Profile)
const instance = new DynamicComponent({
  propsData: {
    name: '李四',
    age: 30
  }
})
instance.$mount()
document.body.appendChild(instance.$el)

// $destroy 销毁实例
instance.$destroy()

补充说明

  • Vue 3 中不再需要 extend,推荐使用 h() 和 render 函数
  • extend 主要用于 Vue 2.x 的插件开发

54. Vue.nextTick

问题:Vue.nextTick

答案核心回答:Vue.nextTick 是 Vue 提供的方法,用于在 DOM 更新后获取最新的 DOM 状态。

详细说明

nextTick 原理

  • Vue 更新 DOM 是异步的
  • nextTick 会在 DOM 更新完成后执行
  • 内部使用 Promise、MutationObserver 或 setTimeout

代码示例

export default {
  data() {
    return { message: 'Hello' }
  },
  methods: {
    async updateMessage() {
      this.message = 'World'
      
      // DOM 尚未更新
      console.log(this.$refs.text.textContent) // Hello
      
      // 使用 nextTick 等待 DOM 更新
      await this.$nextTick()
      
      // DOM 已更新
      console.log(this.$refs.text.textContent) // World
      
      // 也可以传入回调
      this.$nextTick(() => {
        console.log(this.$refs.text.textContent) // World
      })
    }
  }
}

补充说明

  • 也可以使用 this.$nextTick() 或 Vue.nextTick()
  • 在 updated 钩子中不需要使用 nextTick

55. Vue.set

问题:Vue.set

答案核心回答:Vue.set 用于在响应式对象上添加新的响应式属性,确保新增属性也是响应式的。

详细说明

为什么需要 Vue.set

  • Vue 2.x 无法检测对象属性的添加或删除
  • 直接赋值新属性不是响应式的
  • 需要使用 Vue.set 或 this.$set

代码示例

import Vue from 'vue'

export default {
  data() {
    return {
      user: {
        name: '张三'
      }
    }
  },
  methods: {
    addAge() {
      // 直接赋值 - 不是响应式的
      // this.user.age = 25  // 不会触发视图更新
      
      // 使用 set - 是响应式的
      Vue.set(this.user, 'age', 25)
      // 或
      this.$set(this.user, 'age', 25)
    },
    
    addItem() {
      // 数组操作
      Vue.set(this.items, 0, { text: '新项目' })
    }
  }
}

补充说明

  • Vue 3 中直接赋值就是响应式的,不需要 set
  • 对于数组,Vue 2.x 已经包装了 push、splice 等方法

56. Vue.delete

问题:Vue.delete

答案核心回答:Vue.delete 用于删除对象的属性,确保删除操作是响应式的,能触发视图更新。

详细说明

为什么需要 Vue.delete

  • 直接 delete 删除的属性不是响应式的
  • 需要使用 Vue.delete 确保视图更新

代码示例

import Vue from 'vue'

export default {
  data() {
    return {
      user: {
        name: '张三',
        age: 25,
        email: 'zhangsan@example.com'
      }
    }
  },
  methods: {
    removeEmail() {
      // 直接删除 - 不是响应式的
      // delete this.user.email  // 不会触发视图更新
      
      // 使用 delete - 是响应式的
      Vue.delete(this.user, 'email')
      // 或
      this.$delete(this.user, 'email')
    },
    
    removeItem(index) {
      // 数组删除
      this.$delete(this.items, index)
    }
  }
}

补充说明

  • Vue 3 中直接 delete 就是响应式的
  • 删除数组元素推荐使用 splice 或 filter

57. Vue 组件的 data 为什么是函数?

问题:Vue 组件的 data 为什么是函数?

答案核心回答:组件的 data 是函数是为了保证每个组件实例有独立的数据副本,避免多个实例共享同一数据对象导致的状态污染。

详细说明

为什么是函数而不是对象

  • 组件可能被复用多次,每个实例需要独立的数据
  • 如果 data 是对象,所有实例共享同一引用
  • 函数返回一个全新对象,每个实例获得独立数据

代码示例

// 错误写法 - data 是对象
const Component = {
  data: {
    count: 0  // 所有实例共享同一个 count
  }
}

// 正确写法 - data 是函数
const Component = {
  data() {
    return {
      count: 0  // 每个实例有独立的 count
    }
  }
}

// Vue 根实例允许 data 是对象
new Vue({
  data: { count: 0 }  // 根实例只有一次,不需要复用
})

补充说明

  • Vue 2.x 和 Vue 3.x 都遵循这一规则
  • 根实例可以是对象或函数

58. Vue 组件的 name 属性

问题:Vue 组件的 name 属性

答案核心回答:Vue 组件的 name 属性用于标识组件,支持递归调用、路由匹配、组件查找和调试显示。

详细说明

name 属性作用

用途说明
递归组件组件内部可以引用自身
路由匹配配合 keep-alive include/exclude
组件查找$refs、router-view 匹配
DevTools调试工具中显示的名称

代码示例

<!-- 递归组件 - 树形菜单 -->
<template>
  <div class="tree-node">
    <div>{{ node.name }}</div>
    <tree-node 
      v-for="child in node.children" 
      :key="child.id"
      :node="child" 
      name="tree-node"  <!-- 可选,隐式使用自身 name -->
    />
  </div>
</template>

<script>
export default {
  name: 'TreeNode'  // 定义后可以在模板中递归使用
}
</script>

<!-- keep-alive 使用 name -->
<keep-alive include="TreeNode,UserProfile">
  <component :is="currentComponent" />
</keep-alive>

补充说明

  • 组件名推荐使用 PascalCase 或 kebab-case
  • 未定义 name 时,默认使用注册时的名称

59. Vue 组件的 inheritAttrs

问题:Vue 组件的 inheritAttrs

答案核心回答:inheritAttrs 是 Vue 2.4+ 提供的选项,用于控制是否将根元素的属性继承到子组件根元素,默认 true。

详细说明

inheritAttrs 行为

  • 默认 true:非 prop 属性会渲染到根元素
  • 设置为 false:非 prop 属性不会渲染到根元素,但仍可通过 $attrs 访问

代码示例

<!-- 父组件 -->
<my-component 
  class="parent-class"
  data-id="123"
  custom-prop="value"
/>

<!-- 子组件 -->
<script>
export default {
  inheritAttrs: false,  // 关闭默认继承
  mounted() {
    // $attrs 包含所有非 prop 属性
    console.log(this.$attrs) 
    // { 'data-id': '123', 'custom-prop': 'value' }
  }
}
</script>

<!-- inheritAttrs: true 时的渲染结果 -->
<div class="parent-class" data-id="123" custom-prop="value">
  <!-- 非 prop 属性被渲染到根元素 -->
</div>

<!-- inheritAttrs: false 时的渲染结果 -->
<div>
  <!-- 非 prop 属性不会渲染到根元素 -->
  <!-- 但可以通过 $attrs 手动应用 -->
</div>

补充说明

  • 配合 attrsattrs 和 listeners 可以实现透明的属性传递
  • Vue 3 中 inheritAttrs 被移除,默认为 false

60. 函数式组件

问题:函数式组件

答案核心回答:函数式组件是无状态的组件,没有 data 和实例,仅通过 props 渲染内容,性能更高。

详细说明

函数式组件特点

  • 无状态(没有 data)
  • 无实例(没有 this)
  • 仅接收 props
  • 渲染更快,内存更少
  • 不能使用 keep-alive

代码示例

<!-- 函数式组件模板写法 -->
<template functional>
  <div class="icon">
    <i :class="props.icon"></i>
    <span>{{ props.name }}</span>
  </div>
</template>

<script>
export default {
  functional: true,
  props: {
    name: String,
    icon: String
  }
}
</script>

<!-- JSX 写法(更常见) -->
export default {
  functional: true,
  props: ['name', 'icon'],
  render(h, { props, data, children }) {
    return (
      <div class="icon">
        <i class={props.icon}></i>
        <span>{props.name}</span>
      </div>
    )
  }
}

补充说明

  • 适合纯展示的 UI 组件
  • Vue 3 中函数式组件性能优势减小,因为组件本身已经很好优化

动态组件与异步组件

61. 动态组件与异步组件

问题:动态组件与异步组件

答案核心回答:动态组件使用 is 特性切换组件,异步组件使用 Promise 动态加载组件代码。

详细说明

类型实现适用场景
动态组件component + is组件切换
异步组件() => import()按需加载

代码示例

<!-- 动态组件 -->
<component :is="currentTab">
  <!-- 切换 Home、About、Contact 组件 -->
</component>

<!-- 动态组件切换 + keep-alive -->
<keep-alive include="Home,About">
  <component :is="currentTab" />
</keep-alive>

<!-- 异步组件 -->
export default {
  components: {
    // 工厂函数,Promise 形式
    AsyncComponent: () => import('./AsyncComponent.vue'),
    
    // 带加载和错误状态
    AsyncComponentWithOptions: () => ({
      component: import('./AsyncComponent.vue'),
      loading: LoadingComponent,
      error: ErrorComponent,
      delay: 200,
      timeout: 3000
    })
  }
}

补充说明

  • 异步组件会在第一次渲染时才加载
  • 可以配合 webpack 的 import() 实现代码分割

62. 动态组件的实现方式及注意事项

问题:动态组件的实现方式及注意事项

答案核心回答:动态组件通过 component + is 实现,注意事项包括 keep-alive 缓存、组件切换时的状态保持、异步组件加载状态。

详细说明

实现方式

<component :is="currentComponent" />

注意事项

问题解决方案
切换时状态丢失使用 keep-alive
异步组件加载延迟添加 loading 组件
组件加载失败添加 error 组件
切换动画使用 transition

代码示例

<template>
  <div>
    <!-- 动态组件 + keep-alive -->
    <keep-alive include="UserProfile,UserSettings">
      <component :is="currentTab" :key="currentTab" />
    </keep-alive>
    
    <!-- Tab 切换按钮 -->
    <button 
      v-for="tab in tabs" 
      :key="tab"
      :class="{ active: currentTab === tab }"
      @click="currentTab = tab"
    >
      {{ tab }}
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'About', 'Contact']
    }
  },
  components: {
    Home: () => import('./Home.vue'),
    About: () => import('./About.vue'),
    Contact: () => import('./Contact.vue')
  }
}
</script>

补充说明

  • 使用 :is 和组件名或组件对象
  • 配合 v-if 可以控制渲染时机

63. 如何创建递归组件?

问题:如何创建递归组件?

答案核心回答:递归组件是在组件内部使用自身组件,需要设置 name 属性,通过 props 传递数据并设置终止条件。

详细说明

递归组件条件

  • 组件必须设置 name 属性
  • 必须有终止条件,否则无限递归
  • 通常用于树形结构、菜单等

代码示例

<!-- TreeNode.vue - 递归树形菜单 -->
<template>
  <div class="tree-node">
    <div class="node-content" @click="toggle">
      <span>{{ node.name }}</span>
      <span v-if="hasChildren">[{{ isExpanded ? '-' : '+' }}]</span>
    </div>
    
    <!-- 递归调用自身 -->
    <div v-if="hasChildren && isExpanded" class="children">
      <TreeNode 
        v-for="child in node.children" 
        :key="child.id"
        :node="child"
        :depth="depth + 1"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'TreeNode',  // 必须有 name 才能递归
  props: {
    node: {
      type: Object,
      required: true
    },
    depth: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      isExpanded: false
    }
  },
  computed: {
    hasChildren() {
      return this.node.children && this.node.children.length > 0
    }
  },
  methods: {
    toggle() {
      if (this.hasChildren) {
        this.isExpanded = !this.isExpanded
      }
    }
  }
}
</script>

补充说明

  • 递归深度过深可能导致栈溢出
  • 可以通过 depth prop 限制递归深度

插槽(Slots)

64. 插槽

问题:插槽

答案核心回答:插槽是 Vue 组件的占位符,允许父组件向子组件插入内容,实现组件的灵活性复用。

详细说明

插槽类型

类型使用方式说明
默认插槽<slot></slot>插入任意内容
具名插槽<slot name="header">插入到指定位置
作用域插槽<slot :item="item">子组件传递数据

代码示例

<!-- Child.vue -->
<template>
  <div class="container">
    <slot name="header"></slot>
    <slot></slot>  <!-- 默认插槽 -->
    <slot name="footer"></slot>
  </div>
</template>

<!-- Parent.vue -->
<my-component>
  <template v-slot:header>
    <h1>标题</h1>
  </template>
  
  <p>默认插槽内容</p>
  
  <template v-slot:footer>
    <button>按钮</button>
  </template>
</my-component>

补充说明

  • Vue 3 支持 v-slot 简写为 #
  • 插槽内容编译在父组件作用域

65. Vue 插槽

问题:Vue 插槽

答案核心回答:Vue 插槽(slot)是组件内部的占位机制,允许父组件在组件标签内写入内容,实现内容的分发。

详细说明

插槽工作原理

  1. 父组件在子组件标签内编写内容
  2. 子组件的 <slot> 标签作为占位符
  3. 编译后,内容被渲染到 slot 位置

代码示例

<!-- Card.vue - 卡片组件 -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="title">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
    <div class="card-footer">
      <slot name="actions"></slot>
    </div>
  </div>
</template>

<!-- 使用卡片 -->
<card-component>
  <template v-slot:title>自定义标题</template>
  <p>卡片内容</p>
  <template v-slot:actions>
    <button>确定</button>
  </template>
</card-component>

补充说明

  • 插槽可以传递任何模板内容
  • 后备内容(默认内容)在没有提供插槽内容时显示

66. 默认插槽

问题:默认插槽

答案核心回答:默认插槽是没有 name 属性的插槽,用于插入不指定名称的内容。

代码示例

<!-- Alert.vue -->
<template>
  <div class="alert">
    <slot>默认提示信息</slot>  <!-- 默认插槽 -->
  </div>
</template>

<!-- 使用 -->
<alert>自定义内容</alert>
<alert></alert>  <!-- 显示"默认提示信息" -->

补充说明

  • 可以提供后备内容(默认内容)
  • 一个组件只能有一个默认插槽

67. 具名插槽

问题:具名插槽

答案核心回答:具名插槽通过 name 属性区分不同插槽位置,允许父组件精确控制内容插入位置。

代码示例

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>  <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 使用 -->
<layout>
  <template v-slot:header>
    <h1>网站标题</h1>
  </template>
  
  <template v-slot:default>
    <p>页面主要内容</p>
  </template>
  
  <template v-slot:footer>
    <p>版权信息</p>
  </template>
</layout>

补充说明

  • v-slot: 可以简写为 #
  • Vue 3 支持解构插槽 prop

68. 作用域插槽

问题:作用域插槽

答案核心回答:作用域插槽允许子组件向插槽内容传递数据,使父组件可以根据子组件的数据渲染不同内容。

代码示例

<!-- List.vue - 子组件 -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: ['items']
}
</script>

<!-- 使用 - 父组件 -->
<list-component :items="users">
  <template v-slot:default="slotProps">
    <span class="user">
      {{ slotProps.item.name }} - {{ slotProps.index }}
    </span>
  </template>
</list-component>

<!-- 解构写法 (Vue 3) -->
<list-component :items="users">
  <template v-slot:default="{ item, index }">
    <span>{{ item.name }} - {{ index }}</span>
  </template>
</list-component>

补充说明

  • 作用域插槽是实现组件高度复用的利器
  • 典型应用:列表渲染、数据展示组件

69. 插槽的使用场景

问题:插槽的使用场景

答案核心回答:插槽适合布局组件、表单组件、列表组件等需要自定义内容的场景。

详细说明

场景示例插槽内容
布局组件Layoutheader、footer、sidebar
列表组件Table、List行内容自定义
表单组件Form、Input标签、验证信息
弹窗组件Modal标题、内容、操作按钮
卡片组件Card标题、内容、底部操作

代码示例

<!-- Table 组件 - 作用域插槽示例 -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">
          {{ column.label }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="column in columns" :key="column.key">
          <slot :row="row" :column="column" :value="row[column.key]">
            {{ value }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<!-- 使用 Table -->
<data-table :columns="columns" :data="users">
  <template v-slot:default="{ row, column, value }">
    <span v-if="column.key === 'name'">{{ value }}</span>
    <span v-else-if="column.key === 'actions'">
      <button @click="edit(row)">编辑</button>
    </span>
    <span v-else>{{ value }}</span>
  </template>
</data-table>

补充说明

  • 插槽让组件更加灵活可复用
  • 合理设计插槽可以大大减少组件数量

模板语法与指令系统

70. Vue 指令

问题:Vue 指令

答案核心回答:Vue 指令是带有 v- 前缀的特殊属性,用于在模板中添加行为,响应式地操作 DOM。

详细说明

内置指令

指令作用
v-bind动态绑定属性
v-on绑定事件
v-model双向绑定
v-if/v-show条件渲染
v-for列表渲染
v-html插入 HTML
v-text插入文本

代码示例

<template>
  <!-- 绑定属性 -->
  <img v-bind:src="imageSrc" :alt="imageAlt">
  
  <!-- 绑定事件 -->
  <button v-on:click="handleClick">点击</button>
  
  <!-- 双向绑定 -->
  <input v-model="inputValue">
  
  <!-- 条件渲染 -->
  <div v-if="show">显示</div>
  <div v-show="show">v-show</div>
  
  <!-- 列表渲染 -->
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  
  <!-- 插入 HTML -->
  <div v-html="rawHtml"></div>
</template>

补充说明

  • 自定义指令可以通过 Vue.directive() 注册
  • Vue 3 支持组件上使用 v-on 和 v-bind 的修饰符

71. Vue 常用指令

问题:Vue 常用指令

答案核心回答:Vue 常用指令包括 v-bind(绑定)、v-on(事件)、v-model(双向绑定)、v-if/v-show(条件渲染)、v-for(列表渲染)。

详细说明

常用指令速查表

指令语法说明
v-bind:attr="value"绑定属性
v-on@event="handler"绑定事件
v-modelv-model="value"双向绑定
v-ifv-if="condition"条件渲染
v-showv-show="condition"显示/隐藏
v-forv-for="item in items"列表渲染
v-htmlv-html="html"插入 HTML
v-cloakv-cloak防止闪烁

补充说明

  • v-if 是真正的条件渲染,会销毁/重建元素
  • v-show 只是切换 display 样式

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

问题:v-if 与 v-show 的区别

答案核心回答:v-if 是真正的条件渲染,条件为 false 时元素不存在于 DOM;v-show 只是切换 CSS 的 display 属性,元素始终存在于 DOM。

详细说明

对比项v-ifv-show
原理销毁/重建 DOM切换 display
切换开销高(创建/销毁)低(仅样式切换)
初始开销低(条件为 false 不渲染)高(始终渲染)
适用场景运行时很少改变频繁切换
支持 v-else否(可用 v-if)

代码示例

<template>
  <!-- v-if: 条件为 false 时,元素不存在 -->
  <div v-if="isLoggedIn">欢迎回来</div>
  <div v-else>请登录</div>
  
  <!-- v-show: 始终存在于 DOM,只是隐藏 -->
  <div v-show="isVisible">总是渲染</div>
</template>

<script>
export default {
  data() {
    return {
      isLoggedIn: true,
      isVisible: true
    }
  }
}
</script>

补充说明

  • v-if 可以配合 v-else-if、v-else 使用
  • v-if 支持 template 标签,v-show 不支持
  • v-if 在 Vue 3 中支持 key 属性控制复用

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

问题:v-for 与 v-if 的优先级

答案核心回答:v-for 的优先级高于 v-if,这意味着 v-if 会在每个循环迭代中执行,而不是先过滤再循环。

详细说明

优先级问题

  • v-for 和 v-if 同时使用时,每次迭代都会执行 v-if
  • 正确做法:使用 computed 预先过滤,或使用 template + v-for

代码示例

<!-- 错误写法:性能问题 -->
<li v-for="user in users" v-if="user.isActive">
  {{ user.name }}
</li>

<!-- 正确写法1:使用计算属性过滤 -->
<li v-for="user in activeUsers" :key="user.id">
  {{ user.name }}
</li>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: '张三', isActive: true },
        { id: 2, name: '李四', isActive: false }
      ]
    }
  },
  computed: {
    activeUsers() {
      return this.users.filter(user => user.isActive)
    }
  }
}
</script>

<!-- 正确写法2:template + v-for -->
<template v-for="user in users">
  <li v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
</template>

补充说明

  • Vue 3 中 v-if 优先级高于 v-for(与 Vue 2 相反)
  • 建议尽量使用 computed 避免性能问题

74. v-model 的原理

问题:v-model 的原理

答案核心回答:v-model 是 Vue 提供的语法糖,本质是 :value + @input 的组合,在不同表单元素上有不同实现。

详细说明

v-model 原理

元素类型绑定属性触发事件
input textvalueinput
input checkbox/radiocheckedchange
textareavalueinput
selectvaluechange

代码示例

<!-- v-model 本质解析为: -->
<input v-model="message">
<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value">

<!-- checkbox 示例 -->
<input type="checkbox" v-model="checked">
<!-- 等价于 -->
<input type="checkbox" :checked="checked" @change="checked = $event.target.checked">

<!-- 组件上的 v-model (Vue 2) -->
<my-input v-model="value"></my-input>
<!-- 等价于 -->
<my-input :value="value" @input="value = $event"></my-input>

<!-- 组件上的 v-model (Vue 3) -->
<my-input v-model="value"></my-input>
<!-- 等价于 -->
<my-input :modelValue="value" @update:modelValue="value = $event"></my-input>

补充说明

  • 可以使用 modelModifiers 自定义 v-model 行为
  • Vue 3 支持多个 v-model 绑定

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

问题:v-bind 与 v-model 的区别

答案核心回答:v-bind 是单向绑定,数据变化更新视图;v-model 是双向绑定,既可以数据→视图,也可以视图→数据。

详细说明

对比项v-bindv-model
方向单向(数据→视图)双向(数据↔视图)
用途绑定属性、class、style表单元素
本质:attr="value":value + @input
支持所有元素表单元素、组件

代码示例

<template>
  <!-- v-bind: 单向绑定 -->
  <div :title="message">{{ message }}</div>
  
  <!-- v-model: 双向绑定 -->
  <input v-model="message">
  <!-- 等价于 -->
  <input :value="message" @input="message = $event.target.value">
  
  <!-- v-bind 用于组件 props -->
  <child-component :title="pageTitle" />
  
  <!-- 组件上使用 v-model -->
  <child-component v-model="pageTitle" />
</template>

补充说明

  • v-model 是语法糖,v-bind 是基础绑定
  • 非表单元素应使用 v-bind

76. v-on 的修饰符

问题:v-on 的修饰符

答案核心回答:v-on 修饰符用于修改事件行为,如 .stop(阻止冒泡)、.prevent(阻止默认行为)、.enter(按键修饰符)等。

详细说明

事件修饰符

修饰符作用
.stop阻止事件冒泡
.prevent阻止默认行为
.capture使用事件捕获
.self只触发自身
.once只触发一次
.passive不阻止默认滚动

按键修饰符

修饰符作用
.enter回车键
.tabTab 键
.delete删除/退格键
.escEsc 键
.space空格键
.up/down/left/right方向键

代码示例

<template>
  <!-- 阻止冒泡 -->
  <div @click="parentClick">
    <button @click.stop="childClick">点击</button>
  </div>
  
  <!-- 阻止默认行为 -->
  <form @submit.prevent="handleSubmit">
    <button type="submit">提交</button>
  </form>
  
  <!-- 只触发自身 -->
  <div @click.self="handleClick">
    <!-- 只有点击自身才触发 -->
  </div>
  
  <!-- 键盘修饰符 -->
  <input @keyup.enter="handleEnter">
  <input @keyup.esc="handleEsc">
  
  <!-- 组合键 -->
  <input @keyup.ctrl.enter="handleCtrlEnter">
  
  <!-- 鼠标修饰符 -->
  <button @click.right="handleRightClick">右键</button>
  <button @click.middle="handleMiddleClick">中键</button>
</template>

补充说明

  • 修饰符可以串联使用:@click.stop.prevent
  • 可以使用 KeyboardEvent.key 任意值

77. v-slot 的用法

问题:v-slot 的用法

答案核心回答:v-slot 是 Vue 2.6+ 提供的插槽语法,用于命名插槽和作用域插槽,替代了之前的 slot 和 slot-scope。

详细说明

v-slot 语法

用法说明
v-slot:default默认插槽
v-slot:header具名插槽
v-slot:footer="props"作用域插槽
#header简写形式

代码示例

<!-- 子组件 -->
<template>
  <div>
    <slot name="header" :user="user" :age="25"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件使用 -->
<my-component>
  <!-- 具名插槽 -->
  <template v-slot:header="slotProps">
    <h1>{{ slotProps.user.name }}</h1>
  </template>
  
  <!-- 默认插槽 -->
  <template v-slot:default>
    <p>默认内容</p>
  </template>
  
  <!-- 简写 -->
  <template #footer>
    <button>确定</button>
  </template>
</my-component>

补充说明

  • v-slot: 可以简写为 #
  • Vue 3 支持解构:v-slot="{ user }"

78. Vue 指令有哪些?

问题:Vue 指令有哪些?

答案核心回答:Vue 内置指令包括条件渲染(v-if/v-show/v-else)、列表渲染(v-for)、属性绑定(v-bind)、事件绑定(v-on)、双向绑定(v-model)、动态 HTML(v-html/v-text)、其他(v-once/v-cloak/v-pre)。

详细说明

完整指令列表

指令用法说明
v-ifv-if="condition"条件渲染
v-else-ifv-else-if="condition"条件渲染
v-elsev-else条件渲染
v-showv-show="condition"显示隐藏
v-forv-for="item in items"列表渲染
v-bind:attr="value" 或 v-bind:attr属性绑定
v-on@event="handler" 或 v-on:event事件绑定
v-modelv-model="value"双向绑定
v-htmlv-html="html"HTML 内容
v-textv-text="text"文本内容
v-oncev-once只渲染一次
v-prev-pre跳过编译
v-cloakv-cloak防止闪烁

补充说明

  • 可以通过 Vue.directive 注册自定义指令
  • Vue 3 的自定义指令 API 有所变化

79. Vue 指令的作用

问题:Vue 指令的作用

答案核心回答:Vue 指令用于在模板中声明式地操作 DOM,实现响应式的数据绑定、事件处理、条件渲染等功能。

详细说明

指令的核心作用

  1. 数据绑定:将数据与 DOM 建立关联
  2. 事件处理:为 DOM 绑定事件监听
  3. DOM 操作:动态显示/隐藏、增删 DOM
  4. 格式转换:过滤器处理显示格式

代码示例

<template>
  <!-- 数据绑定 -->
  <span :title="message">鼠标悬停</span>
  <span v-text="message"></span>
  
  <!-- 事件处理 -->
  <button @click="handleClick">点击</button>
  <form @submit.prevent="handleSubmit">表单</form>
  
  <!-- 条件渲染 -->
  <p v-if="show">显示</p>
  <p v-show="show">v-show</p>
  
  <!-- 列表渲染 -->
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
  
  <!-- 双向绑定 -->
  <input v-model="inputValue">
</template>

补充说明

  • 指令是 Vue 模板系统的核心组成部分
  • 合理使用指令可以让模板简洁清晰

80. v-pre/v-cloak/v-once 指令的作用

问题:v-pre/v-cloak/v-once 指令的作用

答案核心回答:v-pre 跳过编译显示原始内容;v-cloak 防止 Vue 未加载时的闪烁;v-once 只渲染一次,后续不更新。

详细说明

指令作用使用场景
v-pre跳过编译,显示 {{ }} 原文显示 Vue 语法示例
v-cloak组件编译前隐藏防止初始化闪烁
v-once只渲染一次,静态化静态内容优化

代码示例

<template>
  <!-- v-pre: 显示原始 {{ message }} 不渲染 -->
  <div v-pre>{{ message }} - 不会被编译</div>
  
  <!-- v-cloak: Vue 加载完成后自动移除 -->
  <div v-cloak>
    {{ message }}  <!-- 不会闪烁 -->
  </div>
  
  <!-- v-once: 只渲染一次,之后视为静态 -->
  <div v-once>
    <p>静态内容:{{ staticMessage }}</p>
  </div>
</template>

<style>
[v-cloak] {
  display: none;
}
</style>

补充说明

  • v-cloak 需要配合 CSS 使用
  • v-once 可以减少不必要的渲染,提升性能

81. v-bind 指令的作用

问题:v-bind 指令的作用

答案核心回答:v-bind 用于动态绑定 HTML 属性、class、style、组件 props 等,实现数据驱动的 DOM 更新。

详细说明

v-bind 用法

<!-- 完整写法 -->
<a v-bind:href="url">链接</a>
<img v-bind:src="imageSrc">

<!-- 简写 -->
<a :href="url">链接</a>

<!-- 绑定多个 class -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[activeClass, errorClass]"></div>

<!-- 绑定 style -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyle, customStyle]"></div>

<!-- 绑定对象 -->
<div v-bind="{ id: someId, name: someName }"></div>

<!-- 绑定 props -->
<child :message="parentMessage" />

补充说明

  • 动态 class 和 style 有特殊的绑定语法
  • 可以使用 .prop、.camel、.sync 修饰符

混合(Mixins)与组合式 API

82. Mixins

问题:Mixins

答案核心回答:Mixins 是 Vue 的一种代码复用机制,允许将组件的选项提取为可复用的对象,混入到组件中。

详细说明

Mixins 特点

  • 选项合并:同名选项按策略合并
  • 全局混入:影响所有组件
  • 灵活复用:按需混入

代码示例

// 定义 mixin
const myMixin = {
  data() {
    return {
      message: 'Hello from mixin'
    }
  },
  created() {
    console.log('Mixin created')
  },
  methods: {
    mixinMethod() {
      console.log('Mixin method')
    }
  }
}

// 使用 mixin
const app = new Vue({
  mixins: [myMixin],
  created() {
    console.log('Component created')
  }
})
// 输出:
// Mixin created
// Component created

补充说明

  • Vue 3 推荐使用 Composition API 替代 Mixins
  • Mixins 可能导致命名冲突和来源不清

83. Vue Mixins

问题:Vue Mixins

答案核心回答:Vue Mixins 是一种将组件选项提取为可复用对象的功能,通过 mixins 选项混入到组件中。

详细说明

混入规则

选项合并规则
data浅合并,组件数据优先
methods合并,后者覆盖前者
created合并,都执行,mixin 先
computed合并,同名组件优先

代码示例

// dateMixin.js
export default {
  data() {
    return {
      currentDate: new Date().toLocaleDateString()
    }
  },
  methods: {
    formatDate(date) {
      return new Date(date).toISOString().split('T')[0]
    }
  }
}

// 组件中使用
import dateMixin from './mixins/dateMixin'

export default {
  mixins: [dateMixin],
  mounted() {
    console.log(this.currentDate)
    console.log(this.formatDate('2024-01-01'))
  }
}

补充说明

  • 可以传入多个 mixin
  • 建议使用 mixins 文件夹管理

84. Mixins 的使用

问题:Mixins 的使用

答案核心回答:Mixins 通过 mixins 数组选项使用,支持 data、methods、created 等选项的合并。

代码示例

// alertMixin.js
export default {
  data() {
    return {
      alertMessage: '',
      alertType: 'info'
    }
  },
  methods: {
    showAlert(message, type = 'info') {
      this.alertMessage = message
      this.alertType = type
    },
    hideAlert() {
      this.alertMessage = ''
    }
  }
}

// FormComponent.vue
import alertMixin from '@/mixins/alertMixin'

export default {
  mixins: [alertMixin],
  methods: {
    async handleSubmit() {
      try {
        await api.submit(this.formData)
        this.showAlert('提交成功', 'success')
      } catch (error) {
        this.showAlert('提交失败', 'error')
      }
    }
  }
}

补充说明

  • Mixins 可以是数组,支持多个
  • 同名选项会被合并

85. Mixins 的合并策略

问题:Mixins 的合并策略

答案核心回答:Vue Mixins 的合并策略是:data 对象浅合并,生命周期钩子数组拼接(mixin 先),methods/computed 等对象合并后者覆盖前者。

详细说明

合并策略详情

选项类型合并策略示例
data浅合并{ a: 1, b: 2 } + { b: 3, c: 4 } = { a: 1, b: 3, c: 4 }
生命周期数组拼接,都执行[mixinCreated, componentCreated]
methods后者覆盖前者组件优先
computed合并,同名组件优先同 methods
watch合并,数组拼接[mixinWatcher, componentWatcher]

代码示例

// mixin
const mixin = {
  data() {
    return { message: 'mixin' }
  },
  created() {
    console.log('mixin created')
  }
}

// component
export default {
  mixins: [mixin],
  data() {
    return { message: 'component' }
  },
  created() {
    console.log('component created')
  }
}

// 结果:
// mixin created
// component created
// this.message === 'component'

补充说明

  • 可以通过 Vue.config.optionMergeStrategies 自定义策略
  • 高版本的 Vue 会警告同名选项覆盖

86. 全局 Mixins

问题:全局 Mixins

答案核心回答:全局 Mixins 通过 Vue.mixin() 注册,影响所有 Vue 实例,谨慎使用,通常用于插件开发。

代码示例

// main.js
import Vue from 'vue'

// 全局 mixin - 影响所有组件
Vue.mixin({
  created() {
    console.log('全局 mixin created')
  },
  data() {
    return {
      globalData: '所有组件都有'
    }
  }
})

// 局部 mixin - 只影响当前组件
const myMixin = {
  // ...
}
export default {
  mixins: [myMixin]
}

补充说明

  • 全局 mixin 应谨慎使用,可能影响第三方组件
  • Vue 3 已移除全局 mixin,推荐使用插件和 Composition API

组件系统(组件通信、props、events)

87. Vue 组件

问题:Vue 组件

答案核心回答:Vue 组件是具有独立功能、可复用的 UI 模块,通过 props 接收输入,events 输出,实现模块化开发。

详细说明

组件核心概念

  • Props:接收父组件数据
  • Events:向父组件发送消息
  • Slots:接收分发内容
  • 实例:独立的作用域

代码示例

<!-- Button.vue -->
<template>
  <button 
    :class="['btn', `btn-${type}`]" 
    :disabled="disabled"
    @click="$emit('click', $event)"
  >
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'Button',
  props: {
    type: {
      type: String,
      default: 'primary'
    },
    disabled: Boolean
  }
}
</script>

补充说明

  • 组件名推荐使用 PascalCase 或 kebab-case
  • 单文件组件 (.vue) 是推荐的开发方式

88. Vue 组件的注册方式

问题:Vue 组件的注册方式

答案核心回答:Vue 组件可以通过全局注册(Vue.component)和局部注册(components 选项)两种方式。

详细说明

注册方式作用域适用场景
全局注册所有组件UI 库、通用组件
局部注册当前组件业务组件

代码示例

// 全局注册
import Vue from 'vue'
import Button from './Button.vue'

Vue.component('my-button', Button)  // 自动注册
Vue.component('my-button', {
  template: '<button>按钮</button>'
})

// 局部注册
export default {
  components: {
    Button,  // Button: Button 的简写
    'custom-name': CustomComponent
  }
}

补充说明

  • 全局注册的组件在打包时会包含在 bundle 中
  • 局部注册可以实现更好的 tree-shaking

89. 全局组件与局部组件

问题:全局组件与局部组件

答案核心回答:全局组件在所有组件中可用,通过 Vue.component() 注册;局部组件仅在注册的组件中可用。

代码示例

// 全局注册(通常在 main.js)
Vue.component('global-button', {
  template: '<button>全局按钮</button>'
})

// 局部注册
import LocalComponent from './LocalComponent.vue'

export default {
  components: {
    LocalComponent  // 只在当前组件可用
  }
}

补充说明

  • 全局注册影响打包体积,谨慎使用
  • 业务组件建议使用局部注册

90. 组件通信方式

问题:组件通信方式

答案核心回答:Vue 组件通信方式包括 props/emitemit、parent/childrenchildren、refs、attrs/attrs/listeners、Event Bus、Vuex/Pinia。

详细说明

方式方向适用场景
props/$emit父→子 / 子→父父子通信
parent/parent/children双向访问父子通信
$refs父→子访问子组件实例
provide/inject祖先→后代跨级通信
Event Bus任意组件跨级通信
Vuex/Pinia任意组件全局状态

代码示例

// props/$emit 父子通信
// 父组件
<child-component 
  :message="msg" 
  @update="handleUpdate"
/>

// 子组件
export default {
  props: ['message'],
  methods: {
    send() {
      this.$emit('update', 'new value')
    }
  }
}

补充说明

  • Vue 3 推荐使用 provide/inject 和 Composition API
  • Event Bus 在 Vue 3 中被移除

91. 兄弟组件通信

问题:兄弟组件通信

答案核心回答:兄弟组件通信可以通过父组件作为中转、Event Bus 或 Vuex/Pinia 实现。

详细说明

通信方式

方式原理复杂度
父组件中转父组件传递数据
Event Bus事件总线
Vuex状态管理

代码示例

// 方式1:父组件中转
// Parent.vue
<template>
  <div>
    <component-a @update="handleUpdate" />
    <component-b :value="sharedValue" />
  </div>
</template>

// 方式2:Event Bus
// eventBus.js
import Vue from 'vue'
export const bus = new Vue()

// ComponentA.vue
import { bus } from '@/eventBus'
export default {
  methods: {
    send() {
      bus.$emit('data-updated', newValue)
    }
  }
}

// ComponentB.vue
import { bus } from '@/eventBus'
export default {
  created() {
    bus.$on('data-updated', this.handleUpdate)
  },
  beforeDestroy() {
    bus.$off('data-updated', this.handleUpdate)
  }
}

// 方式3:Vuex(推荐用于复杂应用)

补充说明

  • 简单场景用父组件中转
  • 复杂应用使用 Vuex

92. props 传递数据

问题:props 传递数据

答案核心回答:props 是 Vue 组件的属性,用于接收父组件传递的数据,是父子组件通信的主要方式。

详细说明

props 选项

export default {
  props: {
    // 基础类型
    title: String,
    
    // 多个类型
    id: [String, Number],
    
    // 必填
    name: {
      type: String,
      required: true
    },
    
    // 默认值
    age: {
      type: Number,
      default: 18
    },
    
    // 自定义验证
    score: {
      type: Number,
      validator: value => value >= 0 && value <= 100
    },
    
    // 对象/数组默认值
    items: {
      type: Array,
      default: () => []
    }
  }
}

补充说明

  • props 是单向数据流,父组件变化会自动更新子组件
  • 不应在子组件中直接修改 props

93. props 的验证

问题:props 的验证

答案核心回答:props 验证通过对象形式定义,检查传入的数据是否符合预期,提高组件健壮性。

代码示例

export default {
  props: {
    // 类型检查
    name: String,
    
    // 必填
    email: {
      type: String,
      required: true
    },
    
    // 默认值
    age: {
      type: Number,
      default: 18
    },
    
    // 自定义验证函数
    phone: {
      type: String,
      validator(value) {
        return /^1[3-9]\d{9}$/.test(value)
      }
    },
    
    // 对象类型默认值必须是工厂函数
    user: {
      type: Object,
      default() {
        return { name: '默认用户' }
      }
    },
    
    // 数组默认值
    tags: {
      type: Array,
      default: () => []
    }
  }
}

补充说明

  • 验证失败会在控制台发出警告
  • 生产环境不进行验证,提升性能

94. $emit 触发事件

问题:$emit 触发事件

答案核心回答:$emit 用于子组件向父组件发送消息,通过 v-on 在父组件中监听,是子→父通信的主要方式。

代码示例

<!-- 子组件 -->
<template>
  <button @click="handleClick">点击</button>
</template>

<script>
export default {
  methods: {
    handleClick() {
      // 触发自定义事件
      this.$emit('click')
      
      // 传递参数
      this.$emit('update', { id: 1, name: '张三' })
      
      // 使用 $emit 第二个参数传递数据
      this.$emit('select', 'selected-value')
    }
  }
}
</script>

<!-- 父组件 -->
<template>
  <child-component 
    @click="handleClick"
    @update="handleUpdate"
    @select="handleSelect"
  />
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('子组件点击')
    },
    handleUpdate(payload) {
      console.log('收到:', payload)
    },
    handleSelect(value) {
      console.log('选中:', value)
    }
  }
}
</script>

补充说明

  • 事件名推荐使用 kebab-case
  • Vue 3 推荐使用 emits 选项声明事件

95. parentparent 与 children

问题parentparent 与 children

答案核心回答parent返回当前组件的父组件实例,parent 返回当前组件的父组件实例,children 返回子组件实例数组,用于父子组件直接通信。

代码示例

// 子组件中访问父组件
export default {
  created() {
    console.log(this.$parent)  // 父组件实例
    console.log(this.$parent.title)
    this.$parent.setTitle('新标题')
  }
}

// 父组件中访问子组件
export default {
  mounted() {
    console.log(this.$children)  // 子组件实例数组
    
    // 访问第一个子组件
    this.$children[0].refresh()
    
    // 使用 $refs 更推荐
    console.log(this.$refs.childComponent)
    this.$refs.childComponent.refresh()
  }
}

补充说明

  • $children 不保证顺序,不推荐使用
  • $refs 只在组件渲染完成后填充

96. $refs 的使用

问题:$refs 的使用

答案核心回答refs是组件实例或DOM元素的引用集合,通过ref属性标记,在JS中通过this.refs 是组件实例或 DOM 元素的引用集合,通过 ref 属性标记,在 JS 中通过 this.refs 访问。

代码示例

<template>
  <!-- 访问 DOM 元素 -->
  <input ref="myInput" type="text">
  
  <!-- 访问组件实例 -->
  <child-component ref="child" />
</template>

<script>
export default {
  mounted() {
    // 访问 DOM
    this.$refs.myInput.focus()
    console.log(this.$refs.myInput.value)
    
    // 访问组件
    this.$refs.child.refresh()
    this.$refs.child.$emit('custom-event')
  }
}
</script>

补充说明

  • $refs 只在组件渲染完成后填充
  • 不要在模板中使用 $refs 进行绑定

97. attrsattrs 与 listeners

问题attrsattrs 与 listeners

答案核心回答attrs包含非prop特性,attrs 包含非 prop 特性,listeners 包含父组件传入的事件监听器,用于透传属性和事件。

详细说明

attrsvsattrs vs attrs

属性说明Vue 3 变化
$attrs非 prop 属性合并到 attrs
$listeners事件监听器合并到 $attrs

代码示例

<!-- 父组件 -->
<base-input 
  v-model="inputValue"
  placeholder="请输入"
  class="custom-input"
  @focus="handleFocus"
/>

<!-- BaseInput.vue -->
<template>
  <!-- 继承所有非 prop 属性 -->
  <input 
    v-bind="$attrs"
    v-on="$listeners"
  >
</template>

<script>
export default {
  inheritAttrs: false,
  mounted() {
    console.log(this.$attrs)  // { placeholder: '请输入', class: 'custom-input' }
    console.log(this.$listeners)  // { focus: handleFocus, input: ... }
  }
}
</script>

补充说明

  • 设置 inheritAttrs: false 可以阻止属性透传到根元素
  • Vue 3 中 listeners被合并到listeners 被合并到 attrs

98. Event Bus

问题:Event Bus

答案核心回答:Event Bus 是一个空的 Vue 实例,用于组件间通过事件进行通信,适合小规模跨组件通信。

代码示例

// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 发送事件
import { EventBus } from './eventBus'
EventBus.$emit('user-login', { id: 1, name: '张三' })

// 监听事件
EventBus.$on('user-login', (user) => {
  console.log('用户登录:', user)
})

// 移除监听
EventBus.$off('user-login')

// 在组件中使用
export default {
  mounted() {
    EventBus.$on('update', this.handleUpdate)
  },
  beforeDestroy() {
    EventBus.$off('update', this.handleUpdate)
  }
}

补充说明

  • Vue 3 中移除了 onon、off,推荐使用mitt或tiny-emitter
  • 大型应用推荐使用 Vuex

99. Vuex 状态管理

问题:Vuex 状态管理

答案核心回答:Vuex 是 Vue 的官方状态管理库,提供集中式存储管理组件状态,通过响应式确保状态变化自动更新视图。

代码示例

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: null,
    loading: false
  },
  
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    SET_LOADING(state, loading) {
      state.loading = loading
    }
  },
  
  actions: {
    async login({ commit }, credentials) {
      commit('SET_LOADING', true)
      try {
        const user = await api.login(credentials)
        commit('SET_USER', user)
      } finally {
        commit('SET_LOADING', false)
      }
    }
  },
  
  getters: {
    isLoggedIn: state => !!state.user
  }
})

// 组件中使用
export default {
  computed: {
    ...mapState(['user', 'loading']),
    ...mapGetters(['isLoggedIn'])
  },
  methods: {
    ...mapActions(['login'])
  }
}

补充说明

  • Vue 3 推荐使用 Pinia
  • 严格模式下,mutation 外修改状态会报错

计算属性与侦听器

100. 计算属性

问题:计算属性

答案核心回答:计算属性是基于响应式依赖缓存的计算值,在模板中使用 computed 选项定义,响应式依赖变化时自动重新计算。

详细说明

计算属性 vs Methods

对比项计算属性方法
调用方式像属性一样使用像函数一样调用
缓存有缓存,依赖不变不重算无缓存,每次调用都执行
性能依赖不变时不重复计算每次渲染都执行

代码示例

export default {
  data() {
    return {
      firstName: '张',
      lastName: '三',
      items: [1, 2, 3, 4, 5]
    }
  },
  computed: {
    // 基础用法
    fullName() {
      return this.firstName + this.lastName
    },
    
    // 带 getter/setter
    fullNameWithSetter: {
      get() {
        return this.firstName + this.lastName
      },
      set(value) {
        const [first, last] = value.split('')
        this.firstName = first
        this.lastName = last
      }
    },
    
    // 依赖响应式数据
    filteredItems() {
      return this.items.filter(item => item > 2)
    },
    
    // 计算属性返回对象
    formattedUser() {
      return {
        name: this.firstName + this.lastName,
        isVip: this.items.length > 3
      }
    }
  }
}

补充说明

  • 计算属性默认只有 getter,可以添加 setter
  • 计算属性会基于依赖进行缓存

101. computed 与 methods 的区别

问题:computed 与 methods 的区别

答案核心回答:computed 有缓存机制,依赖不变时返回缓存值;methods 每次调用都会执行,无缓存。

详细说明

对比项computedmethods
调用方式属性访问函数调用
缓存有(依赖不变不重算)
适用场景派生状态处理事件、执行动作
响应式依赖响应式数据自动更新每次渲染都执行

代码示例

<template>
  <!-- computed - 像属性一样使用,有缓存 -->
  <p>{{ fullName }}</p>
  <p>{{ fullName }}</p>  <!-- 第二次使用缓存 -->
  
  <!-- methods - 必须加括号 -->
  <p>{{ getFullName() }}</p>
  <p>{{ getFullName() }}</p>  <!-- 每次都执行 -->
</template>

<script>
export default {
  data() {
    return { firstName: '张', lastName: '三' }
  },
  computed: {
    fullName() {
      console.log('computed 执行')
      return this.firstName + this.lastName
    }
  },
  methods: {
    getFullName() {
      console.log('methods 执行')
      return this.firstName + this.lastName
    }
  }
}
</script>

补充说明

  • 对于不需要缓存的场景使用 methods
  • computed 适合派生状态的计算

102. computed 与 watch 的区别

问题:computed 与 watch 的区别

答案核心回答:computed 用于计算派生值,自动追踪依赖;watch 用于监听数据变化,执行异步或复杂操作。

详细说明

对比项computedwatch
目的计算派生值响应数据变化
使用场景模板中的复杂表达式异步操作、副效应
返回值有(计算结果)无(执行副作用)
缓存

代码示例

// computed - 计算派生状态
export default {
  data() {
    return {
      radius: 10
    }
  },
  computed: {
    circumference() {
      return 2 * Math.PI * this.radius
    },
    area() {
      return Math.PI * this.radius * this.radius
    }
  }
}

// watch - 监听数据变化
export default {
  data() {
    return {
      query: '',
      results: []
    }
  },
  watch: {
    // 基础监听
    query(newQuery, oldQuery) {
      console.log(`查询从 ${oldQuery} 变为 ${newQuery}`)
    },
    
    // 深度监听
    user: {
      handler(newUser, oldUser) {
        console.log('用户信息变化')
      },
      deep: true
    },
    
    // 立即执行
    message: {
      handler(newVal) {
        this.fetchMessage(newVal)
      },
      immediate: true
    },
    
    // 异步操作
    searchQuery: {
      async handler(query) {
        if (query) {
          this.results = await api.search(query)
        }
      },
      debounce: 300  // 需要额外配置
    }
  }
}

补充说明

  • 优先使用 computed,watch 用于需要执行副作用的场景
  • watch 的 debounce 可以配合 lodash 使用

103. watch 的用法

问题:watch 的用法

答案核心回答:watch 用于监听响应式数据的变化,执行异步操作或复杂逻辑,支持深度监听和立即执行。

代码示例

export default {
  data() {
    return {
      userName: '',
      user: {
        profile: {
          age: 0
        }
      },
      dataList: []
    }
  },
  watch: {
    // 基础用法 - 函数形式
    userName(newVal, oldVal) {
      console.log('用户名变化:', newVal)
    },
    
    // 对象形式 - 支持更多选项
    user: {
      handler(newUser) {
        console.log('用户对象变化')
      },
      deep: true,  // 深度监听
      immediate: true  // 立即执行
    },
    
    // 监听嵌套属性
    'user.profile.age'(newAge) {
      console.log('年龄变化:', newAge)
    },
    
    // 监听数组
    dataList: {
      handler(newList) {
        console.log('列表变化,长度:', newList.length)
      },
      deep: true
    }
  }
}

补充说明

  • watch 监听对象时,deep: true 会监听所有嵌套属性
  • immediate 用于组件创建时立即执行一次

104. 深度监听

问题:深度监听

答案核心回答:深度监听通过设置 watch 的 deep: true 选项,监听对象所有嵌套属性的变化。

代码示例

export default {
  data() {
    return {
      form: {
        name: '',
        address: {
          city: '',
          street: ''
        }
      }
    }
  },
  watch: {
    // 深度监听整个对象
    form: {
      handler(newForm) {
        console.log('表单变化:', newForm)
      },
      deep: true
    },
    
    // 只监听特定嵌套属性
    'form.address.city'(newCity) {
      console.log('城市变化:', newCity)
    }
  }
}

补充说明

  • 深度监听性能开销较大,精确监听更高效
  • Vue 3 中 watch 可以直接监听 ref 或 reactive 对象

105. 立即执行监听

问题:立即执行监听

答案核心回答:通过设置 watch 的 immediate: true 选项,可以在组件创建时立即执行一次监听回调。

代码示例

export default {
  data() {
    return {
      query: ''
    }
  },
  watch: {
    query: {
      handler(query) {
        // 组件创建时和 query 变化时都会执行
        console.log('query:', query)
        this.fetchResults(query)
      },
      immediate: true  // 立即执行
    }
  },
  mounted() {
    // immediate: true 使得 watch 在 mounted 之前就执行了
  }
}

补充说明

  • immediate 在组件创建时执行,回调中的 oldValue 是 undefined
  • 适合需要初始化数据的场景

106. 计算属性的缓存

问题:计算属性的缓存

答案核心回答:计算属性基于响应式依赖缓存,只有当依赖变化时才重新计算,相同依赖下多次访问返回缓存值。

代码示例

export default {
  data() {
    return {
      message: 'Hello',
      timestamp: Date.now()
    }
  },
  computed: {
    // 依赖 message,有缓存
    reversedMessage() {
      console.log('计算中...')
      return this.message.split('').reverse().join('')
    },
    
    // 依赖 timestamp,每次都重新计算
    currentTime() {
      return Date.now()
    }
  },
  methods: {
    // 方法每次调用都执行,无缓存
    getReversedMessage() {
      console.log('方法执行中...')
      return this.message.split('').reverse().join('')
    }
  }
}

补充说明

  • Date.now() 不是响应式依赖,不会触发更新
  • Methods 没有缓存,每次渲染都会重新计算

107. 计算属性的 setter

问题:计算属性的 setter

答案核心回答:计算属性可以设置 getter 和 setter,getter 用于读取计算值,setter 用于设置计算值。

代码示例

export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    // 只读计算属性
    fullNameReadOnly() {
      return this.firstName + this.lastName
    },
    
    // 可读可写计算属性
    fullName: {
      get() {
        return this.firstName + this.lastName
      },
      set(value) {
        // value 是设置的新值
        const names = value.split('')
        this.firstName = names[0] || ''
        this.lastName = names.slice(1).join('')
      }
    }
  },
  methods: {
    update() {
      // 使用 setter
      this.fullName = '李四'  // 触发 setter
      console.log(this.firstName)  // '李'
      console.log(this.lastName)  // '四'
    }
  }
}

补充说明

  • setter 在给计算属性赋值时触发
  • 常用于双向绑定的场景

题目统计

  • 总题目数:107
  • 分类数:15