Vue3 高频面试题(下)
56. Pinia 与 Vuex 的区别
56.1 核心概念
- Vuex:包含 State、Getter、Mutation、Action、Module 等概念
- Pinia:包含 State、Getter、Action 等概念,移除了 Mutation
56.2 类型支持
- Vuex:TypeScript 支持不够完善,需要额外配置
- Pinia:原生支持 TypeScript,类型推断更准确
56.3 模块化
- Vuex:需要使用 Module 来组织代码
- Pinia:每个 store 就是一个独立的模块,无需嵌套
56.4 调试
- Vuex:支持时间旅行调试
- Pinia:支持 Vue DevTools,调试体验更好
56.5 状态修改
- Vuex:需要通过 Mutation 修改状态,Action 中提交 Mutation
- Pinia:可以直接在 Action 中修改状态,无需 Mutation
57. Vue Router 4
57.1 Vue Router 4 的新特性
- 组合式 API:支持 useRoute、useRouter 等组合式 API
- TypeScript 支持:更好的 TypeScript 支持
- 动态路由:更灵活的动态路由配置
- 导航守卫:支持异步组件的导航守卫
57.2 useRoute 与 useRouter 的区别
- useRoute:获取当前路由信息
- useRouter:获取路由实例,用于导航
javascript
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
console.log(route.path) // 当前路由路径
router.push('/home') // 导航到 home 页面57.3 导航守卫的使用
- 全局守卫:beforeEach、afterEach
- 路由守卫:beforeEnter
- 组件守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
57.4 动态路由的实现
- 添加路由:router.addRoute()
- 移除路由:router.removeRoute()
- 查看路由:router.getRoutes()
57.5 路由懒加载
- 实现方式:使用 import() 函数动态导入组件
javascript
const Home = () => import('../views/Home.vue')
const routes = [
{ path: '/home', component: Home }
]58. 服务端渲染 (SSR)
58.1 SSR 的优势
- SEO 优化:搜索引擎能更好地爬取页面内容
- 首屏加载速度:减少客户端渲染时间
- 用户体验:更快地看到页面内容
58.2 SSR 的实现方式
- Nuxt.js:基于 Vue 的 SSR 框架
- Vite SSR:Vite 提供的 SSR 支持
58.3 SSR 与 CSR 的区别
- SSR:服务端渲染 HTML,客户端激活
- CSR:客户端渲染 HTML,服务端只提供数据
58.4 SSR 的性能优化
- 缓存:使用缓存策略减少服务器压力
- 预渲染:对静态页面进行预渲染
- 按需加载:只渲染必要的组件
59. 性能优化
59.1 编译时优化
- 静态提升:将静态节点提升到渲染函数外部
- PatchFlag:标记动态节点,减少 diff 范围
- Block Tree:将动态节点分组,提高 diff 效率
59.2 运行时优化
- 使用虚拟列表:处理大量数据的列表
- 避免不必要的响应式转换:使用 markRaw 标记不需要响应式的对象
- 合理使用计算属性:避免重复计算
59.3 组件优化
- 组件拆分:将复杂组件拆分为更小的组件
- 异步组件:按需加载组件
- KeepAlive:缓存组件实例,避免重复渲染
59.4 网络优化
- CDN 加速:使用 CDN 加速静态资源
- 资源压缩:压缩 JS、CSS、图片等资源
- HTTP/2:使用 HTTP/2 协议
60. 错误处理
60.1 全局错误处理
- app.config.errorHandler:捕获全局错误
javascript
app.config.errorHandler = (err, instance, info) => {
// 处理错误
console.error(err)
}60.2 组件错误处理
- onErrorCaptured:捕获组件树中的错误
javascript
import { onErrorCaptured } from 'vue'
setup() {
onErrorCaptured((err, instance, info) => {
// 处理错误
console.error(err)
return false // 阻止错误继续传播
})
}60.3 异步错误处理
- try/catch:捕获异步操作中的错误
javascript
try {
const data = await fetchData()
} catch (error) {
console.error(error)
}61. TypeScript 支持
61.1 Vue3 的 TypeScript 支持
- 原生支持:Vue3 原生支持 TypeScript
- 类型推断:更好的类型推断
- 组件类型:使用 defineComponent 定义组件类型
61.2 组合式 API 的类型支持
- setup 函数:自动推断返回值类型
- ref/reactive:自动推断类型
- props:使用 defineProps 定义 props 类型
61.3 自定义类型
- 接口定义:使用接口定义组件类型
typescript
interface User {
name: string
age: number
}
const user = ref<User>({ name: 'Vue', age: 3 })62. 工具函数
62.1 toRaw 的作用
toRaw:获取响应式对象的原始对象- 使用场景:在不需要响应式的场景下使用原始对象,提高性能
62.2 isRaw 的作用
isRaw:判断一个对象是否是原始对象
62.3 isRef 的作用
isRef:判断一个值是否是 ref 对象
62.4 isReactive 的作用
isReactive:判断一个对象是否是 reactive 对象
62.5 unref 的作用
unref:获取 ref 对象的值,如果不是 ref 对象则直接返回
63. 边界情况处理
63.1 响应式对象的循环引用
- 处理方式:Vue3 使用 WeakMap 存储响应式对象,自动处理循环引用
- 注意事项:避免创建过多的循环引用,可能导致内存泄漏
63.2 大型响应式对象的性能问题
- 解决方案:
- 使用 shallowRef/shallowReactive 避免深度响应式转换
- 使用 markRaw 标记不需要响应式的对象
- 拆分大型对象为多个小型响应式对象
63.3 组件的递归调用
- 实现方式:组件可以递归调用自己
- 注意事项:需要设置递归终止条件,避免无限递归
vue
<template>
<div>
<div>{{ node.name }}</div>
<tree-node v-if="node.children" :nodes="node.children" />
</div>
</template>
<script setup>
const props = defineProps({
nodes: Array
})
</script>64. Vue3 的生态系统
64.1 核心库
- vue:Vue3 核心库
- vue-router:路由管理
- pinia:状态管理
- vue-test-utils:测试工具
64.2 构建工具
- vite:新一代前端构建工具
- webpack:传统前端构建工具
64.3 UI 框架
- Element Plus:Element UI 的 Vue3 版本
- Ant Design Vue:Ant Design 的 Vue3 版本
- Vuetify:Material Design 风格的 UI 框架
65. Vue3 的未来发展
65.1 组合式 API 的普及
- 组合式 API 将成为 Vue3 的主要开发方式
- 更多的第三方库将支持组合式 API
65.2 性能优化
- Vue3 会持续优化性能
- 更好的编译优化和运行时优化
65.3 生态系统的完善
- 更多的工具和库将支持 Vue3
- 更好的开发者体验
66. 实践经验与技巧
66.1 代码组织
- 按功能组织代码:将相关功能的代码放在一起
- 使用自定义 Hooks:封装可复用的逻辑
- 组件拆分:将复杂组件拆分为更小的组件
66.2 调试技巧
- 使用 Vue DevTools:调试 Vue 组件和响应式数据
- 使用 console.log:打印关键数据
- 使用断点:调试复杂逻辑
66.3 性能监控
- 使用 Performance API:监控页面性能
- 使用 Vue DevTools 的性能面板:监控组件渲染性能
67. 常见易错点与解决方案
67.1 忘记使用 .value
- 问题:在 setup 函数中访问 ref 时忘记使用 .value
- 解决方案:记住在 setup 函数中访问 ref 需要使用 .value
67.2 响应式对象的解构赋值
- 问题:解构响应式对象会失去响应式
- 解决方案:使用 toRefs 保持响应式
67.3 深度监听的性能问题
- 问题:对大型对象使用深度监听会影响性能
- 解决方案:避免对大型对象使用深度监听,只监听需要变化的属性
67.4 组件的无限渲染
- 问题:组件无限渲染
- 解决方案:检查是否在渲染函数中修改了响应式数据
68. 大型项目架构设计
68.1 项目结构
- 按功能划分:将项目按功能划分为多个模块
- 公共组件:将公共组件放在单独的目录中
- 工具函数:将工具函数放在单独的目录中
68.2 状态管理
- 使用 Pinia:使用 Pinia 管理全局状态
- 模块化:将状态按模块划分
68.3 路由设计
- 嵌套路由:使用嵌套路由组织页面结构
- 动态路由:使用动态路由处理参数
- 懒加载:按需加载路由组件
68.4 权限管理
- 路由守卫:使用路由守卫控制权限
- 动态路由:根据用户权限动态生成路由
69. 面试场景题
69.1 实现一个响应式的计数器
javascript
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
console.log(count.value) // 0
increment()
console.log(count.value) // 169.2 实现一个自定义 Hook
javascript
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
const updatePosition = (e) => {
x.value = e.clientX
y.value = e.clientY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition)
})
return { x, y }
}69.3 实现一个异步组件
javascript
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})70. Vue3 与其他框架的比较
70.1 Vue3 与 React 的比较
- 响应式系统:Vue3 使用 Proxy,React 使用 useState/useReducer
- 模板语法:Vue3 使用模板语法,React 使用 JSX
- 性能:两者性能相当,但优化方式不同
- 生态系统:React 生态更丰富,Vue 生态更简洁
70.2 Vue3 与 Angular 的比较
- 学习曲线:Vue3 学习曲线更平缓
- 性能:Vue3 性能更好
- 生态系统:Angular 生态更完善,但更复杂
- 灵活性:Vue3 更灵活,Angular 更规范
71. Vue3 的最佳实践
71.1 组件设计
- 单一职责:每个组件只负责一个功能
- 可复用性:设计可复用的组件
- 可维护性:组件代码清晰,易于维护
71.2 响应式数据
- 合理使用 ref 和 reactive:根据数据类型选择合适的响应式 API
- 避免不必要的响应式转换:使用 markRaw 标记不需要响应式的对象
- 合理使用计算属性:避免重复计算
71.3 性能优化
- 使用虚拟列表:处理大量数据的列表
- 使用 KeepAlive:缓存组件实例
- 使用异步组件:按需加载组件
72. 自定义指令的实现
72.1 自定义指令的基本结构
javascript
const vFocus = {
mounted(el) {
el.focus()
}
}
app.directive('focus', vFocus)72.2 自定义指令的生命周期
- beforeMount:指令绑定到元素但未挂载
- mounted:元素已挂载
- beforeUpdate:组件更新前
- updated:组件更新后
- beforeUnmount:元素卸载前
- unmounted:元素已卸载
72.3 自定义指令的参数
javascript
const vColor = {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
}
app.directive('color', vColor)73. 动画与过渡效果
73.1 基本过渡效果
vue
<template>
<transition name="fade">
<div v-if="show">Hello World</div>
</transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>73.2 列表过渡效果
vue
<template>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</transition-group>
</template>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
</style>73.3 使用第三方动画库
vue
<template>
<transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<div v-if="show">Hello World</div>
</transition>
</template>74. 表单处理
74.1 基本表单处理
vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="name" placeholder="Name" />
<input v-model="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('')
const email = ref('')
const handleSubmit = () => {
console.log({ name: name.value, email: email.value })
}
</script>74.2 自定义表单验证
vue
<template>
<form @submit.prevent="handleSubmit">
<input
v-model="name"
placeholder="Name"
:class="{ 'is-invalid': errors.name }"
/>
<div v-if="errors.name" class="error">{{ errors.name }}</div>
<input
v-model="email"
placeholder="Email"
:class="{ 'is-invalid': errors.email }"
/>
<div v-if="errors.email" class="error">{{ errors.email }}</div>
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref, reactive } from 'vue'
const name = ref('')
const email = ref('')
const errors = reactive({})
const validate = () => {
errors.name = name.value ? '' : 'Name is required'
errors.email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value) ? '' : 'Invalid email'
return !errors.name && !errors.email
}
const handleSubmit = () => {
if (validate()) {
console.log({ name: name.value, email: email.value })
}
}
</script>75. 测试
75.1 单元测试
javascript
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('increments when button is clicked', () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})75.2 组件测试
javascript
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
test('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = mount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).toContain(msg)
})76. 构建与部署
76.1 Vite 构建配置
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false
},
server: {
port: 3000,
open: true
}
})76.2 环境变量配置
javascript
// .env.development
VITE_API_URL=http://localhost:3000/api
// .env.production
VITE_API_URL=https://api.example.comjavascript
// 使用环境变量
const apiUrl = import.meta.env.VITE_API_URL77. 国际化 (i18n)
77.1 基本配置
javascript
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
locale: 'en',
messages: {
en: {
hello: 'Hello'
},
zh: {
hello: '你好'
}
}
})
app.use(i18n)77.2 使用方式
vue
<template>
<div>{{ $t('hello') }}</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
console.log(t('hello'))
</script>78. 移动端开发
78.1 响应式布局
vue
<template>
<div class="container">
<div class="content">Content</div>
<div class="sidebar">Sidebar</div>
</div>
</template>
<style>
.container {
display: flex;
}
.content {
flex: 1;
}
.sidebar {
width: 200px;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
}
</style>78.2 触摸事件
vue
<template>
<div @touchstart="handleTouchStart" @touchmove="handleTouchMove"></div>
</template>
<script setup>
const handleTouchStart = (e) => {
console.log('Touch start:', e.touches[0].clientX, e.touches[0].clientY)
}
const handleTouchMove = (e) => {
console.log('Touch move:', e.touches[0].clientX, e.touches[0].clientY)
}
</script>79. 性能优化实战
79.1 虚拟列表实现
vue
<template>
<div class="virtual-list" ref="containerRef" @scroll="handleScroll">
<div class="virtual-list__content" :style="{ height: totalHeight + 'px', paddingTop: offsetY + 'px' }">
<div v-for="item in visibleItems" :key="item.id" class="virtual-list__item">
{{ item.text }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const containerRef = ref(null)
const items = ref([])
const itemHeight = 50
const visibleCount = 20
const offset = ref(0)
// 生成测试数据
for (let i = 0; i < 10000; i++) {
items.value.push({ id: i, text: `Item ${i}` })
}
const totalHeight = computed(() => items.value.length * itemHeight)
const offsetY = computed(() => offset.value * itemHeight)
const visibleItems = computed(() => {
const start = Math.max(0, offset.value)
const end = Math.min(items.value.length, start + visibleCount + 2)
return items.value.slice(start, end)
})
const handleScroll = () => {
if (containerRef.value) {
offset.value = Math.floor(containerRef.value.scrollTop / itemHeight)
}
}
onMounted(() => {
if (containerRef.value) {
containerRef.value.style.height = `${visibleCount * itemHeight}px`
}
})
</script>79.2 图片懒加载
vue
<template>
<img v-for="image in images" :key="image.id" :data-src="image.url" :ref="el => setImageRef(el, image)" class="lazy-image" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const images = ref([])
const imageRefs = ref(new Map())
let observer = null
// 生成测试数据
for (let i = 0; i < 100; i++) {
images.value.push({
id: i,
url: `https://picsum.photos/id/${i}/200/300`
})
}
const setImageRef = (el, image) => {
if (el) {
imageRefs.value.set(image.id, el)
}
}
const loadImage = (el) => {
const src = el.dataset.src
if (!src) return
el.src = src
el.classList.remove('lazy-image')
observer.unobserve(el)
}
onMounted(() => {
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target)
}
})
})
imageRefs.value.forEach(el => {
observer.observe(el)
})
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
</script>
<style>
.lazy-image {
width: 200px;
height: 300px;
background-color: #f0f0f0;
}
</style>80. Vue3 与微前端
80.1 微前端的基本概念
- 微前端:将大型前端应用拆分为多个独立的小型应用
- 优点:独立开发、独立部署、技术栈无关
80.2 Vue3 与 Single-SPA
javascript
// 主应用配置
import { registerApplication, start } from 'single-spa'
registerApplication(
'vue3-app',
() => import('http://localhost:3001/vue3-app.js'),
location => location.pathname.startsWith('/vue3-app')
)
start()javascript
// Vue3 微应用配置
import { createApp } from 'vue'
import App from './App.vue'
import singleSpaVue from 'single-spa-vue'
const appOptions = {
el: '#vue3-app',
render: h => h(App)
}
const vueLifecycles = singleSpaVue({
createApp,
appOptions
})
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount81. 总结
Vue3 是 Vue.js 的新一代版本,带来了许多新特性和改进,包括:
- 基于 Proxy 的响应式系统
- 组合式 API
- 编译优化
- 更好的 TypeScript 支持
- 更灵活的组件设计
掌握 Vue3 的核心概念和最佳实践,对于前端工程师来说非常重要。通过学习和实践,你可以更好地使用 Vue3 构建高性能、可维护的前端应用。