Skip to content

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生态系统的深入理解。

正在精进