Vue 3 Composition API 面试题
Vue 3 Composition API 是现代Vue开发的核心,提供了更灵活的代码组织方式和更好的TypeScript支持。
🔥 Composition API 核心面试题
1. setup函数和响应式系统
问题:详细解释setup函数的工作原理,以及Vue 3响应式系统的实现机制。
参考答案:
vue
<template>
<div>
<h2>用户信息</h2>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<p>计算年份: {{ birthYear }}</p>
<button @click="updateAge">增加年龄</button>
<button @click="fetchUserData">获取用户数据</button>
<p v-if="loading">加载中...</p>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
// 1. 响应式数据
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
const loading = ref(false)
const count = ref(0)
// 2. 计算属性
const birthYear = computed(() => {
return new Date().getFullYear() - user.age
})
// 3. 侦听器
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
},
{ immediate: true }
)
// 4. 方法
const updateAge = () => {
user.age++
}
const fetchUserData = async () => {
loading.value = true
try {
// 模拟API调用
const response = await new Promise(resolve => {
setTimeout(() => {
resolve({
name: '李四',
age: 30,
email: 'lisi@example.com'
})
}, 2000)
})
// 更新响应式数据
Object.assign(user, response)
} catch (error) {
console.error('获取用户数据失败:', error)
} finally {
loading.value = false
}
}
// 5. 生命周期
onMounted(() => {
console.log('组件已挂载')
// DOM操作
nextTick(() => {
console.log('DOM更新完成')
})
})
// 6. 暴露给模板的数据和方法
// 在<script setup>中,默认导出所有顶层变量
</script>关键概念:
reactive(): 深度响应式对象ref(): 基本类型响应式包装computed(): 计算属性watch(): 侦听器onMounted(): 生命周期钩子
2. ref vs reactive 深度对比
问题:ref和reactive的区别,以及在实际开发中的选择策略?
参考答案:
javascript
import { ref, reactive, toRef, toRefs, unref, isRef } from 'vue'
// 1. ref - 基本类型和对象引用
const count = ref(0)
const message = ref('Hello')
const userRef = ref({ name: 'John', age: 25 })
// 访问值需要 .value
console.log(count.value) // 0
count.value++
console.log(userRef.value.name) // 'John'
// 2. reactive - 对象深度响应式
const userReactive = reactive({
name: 'John',
age: 25,
preferences: {
theme: 'dark',
language: 'zh-CN'
}
})
// 直接访问属性
console.log(userReactive.name) // 'John'
userReactive.preferences.theme = 'light' // 深度响应式
// 3. 类型检查和转换
function processValue(value) {
if (isRef(value)) {
return value.value
}
return value
}
// 或者使用 unref
function processValueSimple(value) {
return unref(value) // 如果是ref返回.value,否则返回原值
}
// 4. toRef - 创建对reactive对象属性的ref
const name = toRef(userReactive, 'name')
console.log(name.value) // 'John'
name.value = 'Jane' // 会同时更新userReactive.name
// 5. toRefs - 转换整个reactive对象
function useUser() {
const user = reactive({
name: 'John',
age: 25,
email: 'john@example.com'
})
const updateUser = (updates) => {
Object.assign(user, updates)
}
// 返回时解构,保持响应式
return {
...toRefs(user),
updateUser
}
}
// 使用
const { name, age, email, updateUser } = useUser()
// name, age, email 现在都是ref
// 6. 实际应用场景
// 使用ref的情况:
// - 基本类型:string, number, boolean
// - 需要重新赋值整个对象
// - 与第三方库交互
const isLoading = ref(false)
const currentUser = ref(null)
// 完整替换对象
currentUser.value = await fetchUser()
// 使用reactive的情况:
// - 复杂对象状态
// - 表单数据
// - 配置对象
const form = reactive({
username: '',
password: '',
remember: false,
errors: {}
})
const appConfig = reactive({
theme: 'light',
language: 'zh-CN',
notifications: {
email: true,
push: false
}
})
// 7. 性能考虑
// ref对于对象是浅层响应式(对象本身的引用)
// reactive是深度响应式(对象内部的所有属性)
const shallowRef = ref({
nested: { count: 0 }
})
// 这不会触发更新(浅层)
shallowRef.value.nested.count++
// 这会触发更新(替换整个对象)
shallowRef.value = {
nested: { count: 1 }
}3. 组合式函数(Composables)设计
问题:如何设计可复用的组合式函数,以及常见的设计模式?
参考答案:
javascript
// 1. 鼠标位置追踪
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// 2. 网络请求封装
import { ref, computed } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const isFinished = computed(() => !loading.value)
const isSuccess = computed(() => !error.value && data.value !== null)
const execute = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(unref(url), {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// 支持响应式URL
if (isRef(url)) {
watch(url, execute, { immediate: true })
} else {
execute()
}
return {
data,
error,
loading,
isFinished,
isSuccess,
execute,
refetch: execute
}
}
// 3. 本地存储同步
import { ref, watch, Ref } from 'vue'
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [Ref<T>, (value: T) => void] {
const stored = localStorage.getItem(key)
const value = ref(stored ? JSON.parse(stored) : defaultValue)
const setValue = (newValue: T) => {
value.value = newValue
}
watch(
value,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
},
{ deep: true }
)
return [value, setValue]
}
// 4. 表单验证
import { reactive, computed } from 'vue'
export function useValidation(initialValues, rules) {
const values = reactive({ ...initialValues })
const errors = reactive({})
const touched = reactive({})
const isValid = computed(() => {
return Object.keys(errors).length === 0
})
const validate = (field) => {
if (rules[field]) {
const rule = rules[field]
const value = values[field]
const error = rule(value, values)
if (error) {
errors[field] = error
} else {
delete errors[field]
}
}
}
const validateAll = () => {
Object.keys(rules).forEach(field => {
touched[field] = true
validate(field)
})
return isValid.value
}
const setFieldValue = (field, value) => {
values[field] = value
if (touched[field]) {
validate(field)
}
}
const setFieldTouched = (field) => {
touched[field] = true
validate(field)
}
const resetForm = () => {
Object.assign(values, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
Object.keys(touched).forEach(key => delete touched[key])
}
return {
values,
errors,
touched,
isValid,
setFieldValue,
setFieldTouched,
validateAll,
resetForm
}
}
// 5. 倒计时钩子
import { ref, computed, onUnmounted } from 'vue'
export function useCountdown(initialTime) {
const timeLeft = ref(initialTime)
const isActive = ref(false)
let intervalId = null
const isFinished = computed(() => timeLeft.value <= 0)
const minutes = computed(() => Math.floor(timeLeft.value / 60))
const seconds = computed(() => timeLeft.value % 60)
const start = () => {
if (isActive.value) return
isActive.value = true
intervalId = setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--
} else {
stop()
}
}, 1000)
}
const stop = () => {
isActive.value = false
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
const reset = (time = initialTime) => {
stop()
timeLeft.value = time
}
onUnmounted(() => {
stop()
})
return {
timeLeft,
isActive,
isFinished,
minutes,
seconds,
start,
stop,
reset
}
}
// 6. 组件中使用
import { defineComponent } from 'vue'
import { useMouse, useFetch, useLocalStorage, useValidation, useCountdown } from './composables'
export default defineComponent({
setup() {
// 鼠标位置
const { x, y } = useMouse()
// API数据
const { data: users, loading, error, refetch } = useFetch('/api/users')
// 本地存储
const [theme, setTheme] = useLocalStorage('app-theme', 'light')
// 表单验证
const { values, errors, isValid, setFieldValue, validateAll } = useValidation(
{ username: '', email: '' },
{
username: (value) => !value ? '用户名必填' : null,
email: (value) => !/\S+@\S+\.\S+/.test(value) ? '邮箱格式错误' : null
}
)
// 倒计时
const { timeLeft, isActive, start, stop, reset } = useCountdown(60)
const handleSubmit = () => {
if (validateAll()) {
console.log('表单提交:', values)
}
}
return {
// 鼠标位置
mouseX: x,
mouseY: y,
// API数据
users,
loading,
error,
refetch,
// 主题
theme,
toggleTheme: () => setTheme(theme.value === 'light' ? 'dark' : 'light'),
// 表单
values,
errors,
isValid,
setFieldValue,
handleSubmit,
// 倒计时
timeLeft,
isActive,
startCountdown: start,
stopCountdown: stop,
resetCountdown: reset
}
}
})4. watchEffect 和 watch 深度对比
问题:watchEffect和watch的区别,以及各自的使用场景?
参考答案:
javascript
import { ref, reactive, watch, watchEffect, computed, nextTick } from 'vue'
const count = ref(0)
const user = reactive({ name: 'John', age: 25 })
// 1. watch - 明确指定依赖
watch(count, (newValue, oldValue) => {
console.log(`count从 ${oldValue} 变为 ${newValue}`)
})
// 监听多个源
watch(
[count, () => user.name],
([newCount, newName], [oldCount, oldName]) => {
console.log('多个依赖变化')
}
)
// 深度监听对象
watch(
user,
(newUser, oldUser) => {
console.log('用户信息变化:', newUser)
},
{ deep: true, immediate: true }
)
// 2. watchEffect - 自动依赖追踪
watchEffect(() => {
console.log(`自动追踪: count=${count.value}, name=${user.name}`)
// 函数内部使用的响应式数据都会被追踪
})
// 3. 实际应用场景
// 场景1: API同步
const userId = ref(1)
const userProfile = ref(null)
// 使用 watch
watch(
userId,
async (newId) => {
if (newId) {
userProfile.value = await fetchUserProfile(newId)
}
},
{ immediate: true }
)
// 使用 watchEffect
watchEffect(async () => {
if (userId.value) {
userProfile.value = await fetchUserProfile(userId.value)
}
})
// 场景2: 本地存储同步
const settings = reactive({
theme: 'light',
language: 'zh-CN'
})
// watchEffect 自动同步所有设置
watchEffect(() => {
localStorage.setItem('app-settings', JSON.stringify(settings))
})
// 场景3: 副作用清理
const keyword = ref('')
watchEffect((onInvalidate) => {
const controller = new AbortController()
// 搜索API调用
if (keyword.value) {
fetch(`/api/search?q=${keyword.value}`, {
signal: controller.signal
}).then(response => {
// 处理响应
}).catch(error => {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error)
}
})
}
// 清理函数
onInvalidate(() => {
controller.abort()
})
})
// 场景4: 条件性侦听
const isEnabled = ref(true)
const data = ref([])
let stopWatcher = null
watchEffect(() => {
if (isEnabled.value) {
// 开始监听
stopWatcher = watch(
data,
(newData) => {
console.log('数据更新:', newData)
},
{ deep: true }
)
} else {
// 停止监听
if (stopWatcher) {
stopWatcher()
stopWatcher = null
}
}
})
// 场景5: DOM更新后的操作
const list = ref([])
const listRef = ref(null)
watchEffect(
() => {
if (list.value.length > 0 && listRef.value) {
// DOM已更新,可以进行DOM操作
listRef.value.scrollTop = listRef.value.scrollHeight
}
},
{ flush: 'post' } // DOM更新后执行
)
// 场景6: 调试和开发
if (process.env.NODE_ENV === 'development') {
watchEffect(() => {
console.log('开发模式调试:', {
count: count.value,
user: user.name,
timestamp: Date.now()
})
})
}
// 7. 性能对比和选择策略
// 使用 watch 的情况:
// - 需要访问旧值
// - 需要在特定条件下才执行副作用
// - 明确知道依赖项,且依赖项较少
// - 需要精确控制触发时机
// 使用 watchEffect 的情况:
// - 副作用函数依赖多个响应式数据
// - 不需要旧值
// - 希望简化代码,自动依赖追踪
// - 初始化时就需要执行一次
// 8. 高级用法
const stopWatchEffect = watchEffect(() => {
// 一些副作用操作
})
// 手动停止
setTimeout(() => {
stopWatchEffect()
}, 5000)
// 异步副作用处理
watchEffect(async (onInvalidate) => {
let cancelled = false
onInvalidate(() => { cancelled = true })
const result = await someAsyncOperation()
if (!cancelled) {
// 处理结果
}
})
async function someAsyncOperation() {
// 模拟异步操作
return new Promise(resolve => setTimeout(resolve, 1000))
}
async function fetchUserProfile(id) {
// 模拟API调用
return { id, name: `User${id}`, email: `user${id}@example.com` }
}这些Vue 3 Composition API面试题涵盖了现代Vue开发的核心概念和最佳实践,展示了对Vue生态系统的深入理解。
