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、指令系统、轻量级等核心特点。
详细说明:
- 响应式数据绑定:Vue 实现了一套响应式系统,当数据变化时,视图会自动更新,无需手动操作 DOM。
- 组件系统:Vue 提供强大的组件系统,支持组件的复用、嵌套和通信。
- 虚拟 DOM:通过虚拟 DOM 技术,最小化 DOM 操作,提高渲染性能。
- 指令系统:内置 v-if、v-for、v-bind、v-on 等指令,方便操作 DOM。
- 单文件组件:使用 .vue 文件格式,可以在一个文件中编写模板、脚本和样式。
- 渐进式框架:可以只使用核心库,也可以配合 Vuex、Vue Router 等生态使用。
- 轻量级: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 元素,封装可重用的代码。
使用组件的好处:
- 代码复用:相同的 UI 结构只需编写一次
- 易于维护:修改一个组件会影响所有使用它的地方
- 独立作用域:每个组件有独立的数据和方法
- 便于测试:组件可以独立进行单元测试
- 团队协作:不同团队可以并行开发不同组件
代码示例:
<!-- 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 和 View | Vue 实例 |
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 方案:
- 安装依赖:vue-server-renderer、express
- 创建服务器入口
- 创建 webpack 配置
- 配置路由和 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 流程:
- 服务端接收请求
- 运行 Vue 组件代码,渲染成 HTML 字符串
- 返回 HTML 给浏览器显示
- 浏览器下载 JS 资源
- 客户端 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 | 需要完美 SEO | Nuxt.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+ 代码编译打包为浏览器可运行的静态资源。
详细说明:
构建流程:
- 安装依赖(npm install)
- 执行构建命令(npm run build)
- webpack/Vite 读取入口配置
- 递归解析模块依赖
- 编译、压缩、合并资源
- 输出到 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 格式 | 原理 | 特点 |
|---|---|---|---|
| hash | example.com/#/path | 监听 hashchange 事件 | 无需服务器配置 |
| history | example.com/path | HTML5 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. route 的区别
问题:route 的区别
答案: 核心回答:route 是当前路由对象,包含路由信息(params、query、meta 等)。
详细说明:
| 对象 | 类型 | 用途 |
|---|---|---|
| $router | VueRouter 实例 | 编程式导航、操作路由 |
| $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
补充说明:
- route 是当前活跃的路由
- 在模板中使用时不需要加 this
Vue 原理
26. Vue 依赖收集
问题:Vue 依赖收集
答案: 核心回答:Vue 通过数据劫持和订阅者模式实现依赖收集,当数据变化时自动通知所有依赖进行更新。
详细说明:
原理流程:
- 在 render 函数中访问数据时,触发 getter
- getter 中将当前渲染 watcher 添加到数据的依赖列表
- 数据变化时,触发 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,触发异步更新队列。
详细说明:
更新流程:
- 数据变化触发 setter
- setter 调用 dep.notify()
- 所有订阅的 watcher 被放入更新队列
- 通过 nextTick 异步执行所有 watcher.update()
- 触发组件重新渲染
代码示例:
// 派发更新实现
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 中执行。
详细说明:
异步更新原理:
- 响应式数据变化时,触发 watcher 更新
- watcher 更新被放入队列(queueWatcher)
- 队列去重,避免重复更新
- nextTick 时统一执行队列中的更新
- 执行完成后,触发组件重新渲染
代码示例:
// 异步更新示例
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 策略:
- 同层比较:只比较同一层级的节点,不跨层比较
- 先比较标签:标签不同则直接替换
- 同标签比较:比较属性和子节点
- 使用 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)三个阶段。
详细说明:
编译三阶段:
| 阶段 | 输出 | 说明 |
|---|---|---|
| parse | AST 抽象语法树 | 解析模板字符串为 AST |
| optimize | 优化后的 AST | 标记静态节点,跳过 diff |
| generate | render 函数 | 生成可执行代码字符串 |
代码示例:
// 模板编译示例
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 更新前 |
| 更新 | updated | DOM 更新完成 |
| 销毁 | 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、添加事件监听 |
| updated | DOM 操作、状态同步 |
| 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。
详细说明:
| 对比项 | created | mounted |
|---|---|---|
| 执行时机 | 实例创建完成,数据已绑定 | 实例挂载到 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 实例从创建、运行到销毁的过程,期间会触发一系列生命周期钩子,让开发者有机会在各个阶段执行逻辑。
详细说明:
完整生命周期:
- new Vue() - 创建实例
- beforeCreate - 初始化注入和响应式
- created - 实例创建完成
- 编译模板 - 挂载前准备
- beforeMount - 挂载前
- mounted - 挂载到 DOM
- 运行中 - 数据变化时
- beforeUpdate - 更新前
- updated - 更新后
- 销毁 - 组件卸载时
- beforeDestroy - 销毁前
- 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 Shaking | production 模式 | 移除未使用代码 |
| Gzip | configureWebpack | 减少传输体积 |
| 外部依赖 | externals | 不打入 bundle |
| Source Map | productionSourceMap | 减小体积 |
代码示例:
// 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 访问状态或提交变更。
详细说明:
使用步骤:
- 安装 Vuex:npm install vuex
- 创建 store 文件
- 在 main.js 中注册
- 在组件中访问状态
代码示例:
// 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:
| 对比项 | Action | Mutation |
|---|---|---|
| 异步操作 | 支持 | 不支持 |
| 调用方式 | dispatch | commit |
| 参数 | context + payload | state + 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>
补充说明:
- 配合 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)是组件内部的占位机制,允许父组件在组件标签内写入内容,实现内容的分发。
详细说明:
插槽工作原理:
- 父组件在子组件标签内编写内容
- 子组件的 <slot> 标签作为占位符
- 编译后,内容被渲染到 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. 插槽的使用场景
问题:插槽的使用场景
答案: 核心回答:插槽适合布局组件、表单组件、列表组件等需要自定义内容的场景。
详细说明:
| 场景 | 示例 | 插槽内容 |
|---|---|---|
| 布局组件 | Layout | header、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-model | v-model="value" | 双向绑定 |
| v-if | v-if="condition" | 条件渲染 |
| v-show | v-show="condition" | 显示/隐藏 |
| v-for | v-for="item in items" | 列表渲染 |
| v-html | v-html="html" | 插入 HTML |
| v-cloak | v-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-if | v-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 text | value | input |
| input checkbox/radio | checked | change |
| textarea | value | input |
| select | value | change |
代码示例:
<!-- 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-bind | v-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 | 回车键 |
| .tab | Tab 键 |
| .delete | 删除/退格键 |
| .esc | Esc 键 |
| .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-if | v-if="condition" | 条件渲染 |
| v-else-if | v-else-if="condition" | 条件渲染 |
| v-else | v-else | 条件渲染 |
| v-show | v-show="condition" | 显示隐藏 |
| v-for | v-for="item in items" | 列表渲染 |
| v-bind | :attr="value" 或 v-bind:attr | 属性绑定 |
| v-on | @event="handler" 或 v-on:event | 事件绑定 |
| v-model | v-model="value" | 双向绑定 |
| v-html | v-html="html" | HTML 内容 |
| v-text | v-text="text" | 文本内容 |
| v-once | v-once | 只渲染一次 |
| v-pre | v-pre | 跳过编译 |
| v-cloak | v-cloak | 防止闪烁 |
补充说明:
- 可以通过 Vue.directive 注册自定义指令
- Vue 3 的自定义指令 API 有所变化
79. Vue 指令的作用
问题:Vue 指令的作用
答案: 核心回答:Vue 指令用于在模板中声明式地操作 DOM,实现响应式的数据绑定、事件处理、条件渲染等功能。
详细说明:
指令的核心作用:
- 数据绑定:将数据与 DOM 建立关联
- 事件处理:为 DOM 绑定事件监听
- DOM 操作:动态显示/隐藏、增删 DOM
- 格式转换:过滤器处理显示格式
代码示例:
<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/parent/refs、listeners、Event Bus、Vuex/Pinia。
详细说明:
| 方式 | 方向 | 适用场景 |
|---|---|---|
| props/$emit | 父→子 / 子→父 | 父子通信 |
| 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. children
问题:children
答案: 核心回答: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 访问。
代码示例:
<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. listeners
问题:listeners
答案: 核心回答:listeners 包含父组件传入的事件监听器,用于透传属性和事件。
详细说明:
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 中 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 中移除了 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 每次调用都会执行,无缓存。
详细说明:
| 对比项 | computed | methods |
|---|---|---|
| 调用方式 | 属性访问 | 函数调用 |
| 缓存 | 有(依赖不变不重算) | 无 |
| 适用场景 | 派生状态 | 处理事件、执行动作 |
| 响应式 | 依赖响应式数据自动更新 | 每次渲染都执行 |
代码示例:
<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 用于监听数据变化,执行异步或复杂操作。
详细说明:
| 对比项 | computed | watch |
|---|---|---|
| 目的 | 计算派生值 | 响应数据变化 |
| 使用场景 | 模板中的复杂表达式 | 异步操作、副效应 |
| 返回值 | 有(计算结果) | 无(执行副作用) |
| 缓存 | 有 | 无 |
代码示例:
// 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