Vue 3 面试题 - 状态管理与数据流

目录

  1. [Pinia 状态管理](#Pinia 状态管理)
  2. 组件通信
  3. 依赖注入
  4. [Composables 复用](#Composables 复用)
  5. 响应式数据流

Pinia 状态管理

Q1: Pinia 相比 Vuex 有什么优势?如何使用 Pinia?

详细解答:

Pinia vs Vuex 对比

特性 Pinia Vuex
API 风格 Composition API 风格 Options API 风格
TypeScript 支持 完美支持,自动类型推导 需要额外配置
Mutations 不需要 mutations 需要 mutations
模块化 自动模块化 需要手动配置 modules
DevTools 支持 完整支持 完整支持
代码分割 自动 需要手动配置
体积 更小(约 1kb) 相对较大
SSR 支持 内置支持 需要额外配置

Pinia 基础用法

1. 安装和配置

bash 复制代码
npm install pinia
javascript 复制代码
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

2. 定义 Store

javascript 复制代码
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Option Store 写法
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // 访问其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
    
    // 接受参数的 getter
    getUserById: (state) => {
      return (userId) => state.users.find(user => user.id === userId)
    }
  },
  
  actions: {
    increment() {
      this.count++
    },
    
    async fetchData() {
      const response = await fetch('/api/data')
      const data = await response.json()
      this.count = data.count
    },
    
    // 访问其他 store
    async crossStoreAction() {
      const userStore = useUserStore()
      await userStore.fetchUser()
    }
  }
})

// Setup Store 写法(推荐)
export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const name = ref('Counter')
  
  // getters
  const doubleCount = computed(() => count.value * 2)
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)
  
  // actions
  function increment() {
    count.value++
  }
  
  async function fetchData() {
    const response = await fetch('/api/data')
    const data = await response.json()
    count.value = data.count
  }
  
  return {
    count,
    name,
    doubleCount,
    doubleCountPlusOne,
    increment,
    fetchData
  }
})

3. 在组件中使用

vue 复制代码
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">增加</button>
    <button @click="handleIncrement">增加(解构)</button>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// ❌ 错误:直接解构会失去响应式
// const { count, doubleCount } = counter

// ✅ 正确:使用 storeToRefs 保持响应式
const { count, doubleCount } = storeToRefs(counter)

// Actions 可以直接解构
const { increment } = counter

function handleIncrement() {
  increment()
}
</script>

4. 修改 State

javascript 复制代码
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// 方法 1: 直接修改
counter.count++

// 方法 2: 使用 $patch (对象)
counter.$patch({
  count: counter.count + 1,
  name: 'New Counter'
})

// 方法 3: 使用 $patch (函数)
counter.$patch((state) => {
  state.count++
  state.name = 'New Counter'
})

// 方法 4: 替换整个 state
counter.$state = {
  count: 10,
  name: 'Reset Counter'
}

// 方法 5: 调用 action
counter.increment()

5. 订阅 State 变化

javascript 复制代码
import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// 方法 1: 使用 $subscribe
counter.$subscribe((mutation, state) => {
  console.log('Store changed:', mutation.type)
  console.log('New state:', state)
  
  // 持久化到 localStorage
  localStorage.setItem('counter', JSON.stringify(state))
})

// 方法 2: 使用 watch
watch(
  () => counter.count,
  (newCount) => {
    console.log('Count changed:', newCount)
  }
)

// 方法 3: 订阅 actions
counter.$onAction(({
  name,      // action 名称
  store,     // store 实例
  args,      // 传递给 action 的参数
  after,     // action 执行后的钩子
  onError    // action 错误时的钩子
}) => {
  console.log(`Action ${name} called with args:`, args)
  
  after((result) => {
    console.log('Action completed, result:', result)
  })
  
  onError((error) => {
    console.error('Action error:', error)
  })
})

6. 插件系统

javascript 复制代码
// plugins/persistedState.js
export function persistedStatePlugin({ store }) {
  // 从 localStorage 恢复状态
  const savedState = localStorage.getItem(store.$id)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }
  
  // 订阅状态变化并保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

// main.js
import { createPinia } from 'pinia'
import { persistedStatePlugin } from './plugins/persistedState'

const pinia = createPinia()
pinia.use(persistedStatePlugin)

7. 复杂示例 - 用户管理 Store

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || '')
  const permissions = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || 'Guest')
  const hasPermission = computed(() => {
    return (permission) => permissions.value.includes(permission)
  })
  
  // Actions
  async function login(credentials) {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      
      if (!response.ok) {
        throw new Error('登录失败')
      }
      
      const data = await response.json()
      
      token.value = data.token
      user.value = data.user
      permissions.value = data.permissions
      
      // 保存 token
      localStorage.setItem('token', data.token)
      
      return true
    } catch (err) {
      error.value = err.message
      return false
    } finally {
      loading.value = false
    }
  }
  
  async function logout() {
    try {
      await fetch('/api/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token.value}`
        }
      })
    } finally {
      // 清除状态
      user.value = null
      token.value = ''
      permissions.value = []
      
      // 清除本地存储
      localStorage.removeItem('token')
    }
  }
  
  async function fetchUserProfile() {
    if (!token.value) return
    
    loading.value = true
    
    try {
      const response = await fetch('/api/user/profile', {
        headers: {
          'Authorization': `Bearer ${token.value}`
        }
      })
      
      if (!response.ok) {
        throw new Error('获取用户信息失败')
      }
      
      const data = await response.json()
      user.value = data
    } catch (err) {
      error.value = err.message
      
      // Token 可能已失效
      if (err.message.includes('401')) {
        await logout()
      }
    } finally {
      loading.value = false
    }
  }
  
  function $reset() {
    user.value = null
    token.value = ''
    permissions.value = []
    loading.value = false
    error.value = null
  }
  
  return {
    // State
    user,
    token,
    permissions,
    loading,
    error,
    
    // Getters
    isLoggedIn,
    userName,
    hasPermission,
    
    // Actions
    login,
    logout,
    fetchUserProfile,
    $reset
  }
})

组件通信

Q2: Vue 3 中有哪些组件通信方式?各自的使用场景是什么?

详细解答:

1. Props / Emits(父子组件)

基本用法:

vue 复制代码
<!-- Parent.vue -->
<template>
  <Child 
    :message="parentMessage"
    :count="count"
    @update="handleUpdate"
    @custom-event="handleCustomEvent"
  />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const parentMessage = ref('Hello from parent')
const count = ref(0)

function handleUpdate(newValue) {
  console.log('Received update:', newValue)
}

function handleCustomEvent(payload) {
  console.log('Custom event:', payload)
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
    <button @click="sendUpdate">发送更新</button>
    <button @click="sendCustom">自定义事件</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

// 定义 props
const props = defineProps({
  message: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 定义 emits
const emit = defineEmits(['update', 'custom-event'])

function sendUpdate() {
  emit('update', 'New value')
}

function sendCustom() {
  emit('custom-event', { data: 'payload' })
}
</script>

TypeScript 支持:

vue 复制代码
<script setup lang="ts">
interface Props {
  message: string
  count?: number
  user?: {
    id: number
    name: string
  }
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()
</script>

2. v-model(双向绑定)

基本 v-model:

vue 复制代码
<!-- Parent.vue -->
<template>
  <CustomInput v-model="inputValue" />
  <p>Value: {{ inputValue }}</p>
</template>

<script setup>
import { ref } from 'vue'

const inputValue = ref('')
</script>

<!-- CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

多个 v-model:

vue 复制代码
<!-- Parent.vue -->
<template>
  <UserForm
    v-model:first-name="firstName"
    v-model:last-name="lastName"
    v-model:email="email"
  />
</template>

<script setup>
import { ref } from 'vue'

const firstName = ref('')
const lastName = ref('')
const email = ref('')
</script>

<!-- UserForm.vue -->
<template>
  <input
    :value="firstName"
    @input="emit('update:firstName', $event.target.value)"
    placeholder="First Name"
  />
  <input
    :value="lastName"
    @input="emit('update:lastName', $event.target.value)"
    placeholder="Last Name"
  />
  <input
    :value="email"
    @input="emit('update:email', $event.target.value)"
    placeholder="Email"
  />
</template>

<script setup>
defineProps(['firstName', 'lastName', 'email'])
const emit = defineEmits(['update:firstName', 'update:lastName', 'update:email'])
</script>

v-model 修饰符:

vue 复制代码
<!-- Parent.vue -->
<template>
  <CustomInput v-model.capitalize="text" />
</template>

<!-- CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="handleInput"
  />
</template>

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: {
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])

function handleInput(event) {
  let value = event.target.value
  
  // 应用修饰符
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  
  emit('update:modelValue', value)
}
</script>

3. Refs(父组件调用子组件方法)

vue 复制代码
<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  childRef.value.childMethod()
  console.log(childRef.value.childData)
}
</script>

<!-- Child.vue -->
<template>
  <div>Child Component</div>
</template>

<script setup>
import { ref, defineExpose } from 'vue'

const childData = ref('Child data')

function childMethod() {
  console.log('Child method called')
}

// 暴露给父组件
defineExpose({
  childData,
  childMethod
})
</script>

4. Provide / Inject(跨层级通信)

基本用法:

vue 复制代码
<!-- Grandparent.vue -->
<template>
  <Parent />
</template>

<script setup>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'

const theme = ref('dark')
const user = ref({ name: 'Alice' })

// 提供数据
provide('theme', theme)
provide('user', user)

// 提供方法
provide('updateTheme', (newTheme) => {
  theme.value = newTheme
})
</script>

<!-- Grandchild.vue (任意深度的子组件) -->
<template>
  <div :class="theme">
    <p>User: {{ user.name }}</p>
    <button @click="updateTheme('light')">切换主题</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')

// 提供默认值
const optionalValue = inject('optional', 'default value')

// 使用工厂函数提供默认值
const computedDefault = inject('key', () => computeExpensiveDefault())
</script>

TypeScript 支持:

typescript 复制代码
// types.ts
import type { InjectionKey, Ref } from 'vue'

export interface User {
  id: number
  name: string
}

export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
export const userKey: InjectionKey<Ref<User>> = Symbol('user')

// Provider.vue
import { provide, ref } from 'vue'
import { themeKey, userKey } from './types'

const theme = ref('dark')
const user = ref({ id: 1, name: 'Alice' })

provide(themeKey, theme)
provide(userKey, user)

// Consumer.vue
import { inject } from 'vue'
import { themeKey, userKey } from './types'

const theme = inject(themeKey) // 类型:Ref<string> | undefined
const user = inject(userKey)   // 类型:Ref<User> | undefined

5. Event Bus(兄弟组件)

使用 mitt 库:

bash 复制代码
npm install mitt
javascript 复制代码
// eventBus.js
import mitt from 'mitt'

export const emitter = mitt()

// ComponentA.vue
<template>
  <button @click="sendMessage">发送消息</button>
</template>

<script setup>
import { emitter } from './eventBus'

function sendMessage() {
  emitter.emit('message', { text: 'Hello from A' })
}
</script>

// ComponentB.vue
<template>
  <p>{{ receivedMessage }}</p>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'

const receivedMessage = ref('')

function handleMessage(payload) {
  receivedMessage.value = payload.text
}

onMounted(() => {
  emitter.on('message', handleMessage)
})

onUnmounted(() => {
  emitter.off('message', handleMessage)
})
</script>

依赖注入

Q3: Provide / Inject 的高级用法和最佳实践?

详细解答:

1. 响应式注入

vue 复制代码
<!-- Provider.vue -->
<template>
  <div>
    <button @click="count++">增加</button>
    <Child />
  </div>
</template>

<script setup>
import { provide, ref, readonly } from 'vue'

const count = ref(0)

// ✅ 提供响应式数据
provide('count', count)

// ✅ 提供只读响应式数据(防止子组件修改)
provide('readonlyCount', readonly(count))

// ✅ 提供修改方法
provide('increment', () => count.value++)
</script>

<!-- Consumer.vue -->
<script setup>
import { inject } from 'vue'

const count = inject('count')
const readonlyCount = inject('readonlyCount')
const increment = inject('increment')

// ❌ 这样会报错(只读)
// readonlyCount.value++

// ✅ 使用提供的方法修改
increment()
</script>

2. 应用级 Provide

javascript 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 全局配置
app.provide('config', {
  apiUrl: 'https://api.example.com',
  timeout: 5000
})

// 全局服务
app.provide('apiService', {
  async get(url) {
    const response = await fetch(url)
    return response.json()
  }
})

app.mount('#app')

3. 组合式函数中使用

javascript 复制代码
// composables/useTheme.js
import { inject, provide, ref, readonly } from 'vue'

const ThemeSymbol = Symbol('theme')

// 提供主题
export function provideTheme() {
  const theme = ref('light')
  const isDark = computed(() => theme.value === 'dark')
  
  function toggleTheme() {
    theme.value = isDark.value ? 'light' : 'dark'
  }
  
  provide(ThemeSymbol, {
    theme: readonly(theme),
    isDark,
    toggleTheme
  })
  
  return {
    theme,
    isDark,
    toggleTheme
  }
}

// 使用主题
export function useTheme() {
  const themeContext = inject(ThemeSymbol)
  
  if (!themeContext) {
    throw new Error('useTheme must be used within a theme provider')
  }
  
  return themeContext
}

// App.vue
<script setup>
import { provideTheme } from './composables/useTheme'

provideTheme()
</script>

// AnyComponent.vue
<script setup>
import { useTheme } from './composables/useTheme'

const { theme, isDark, toggleTheme } = useTheme()
</script>

Composables 复用

Q4: 如何编写高质量的 Composable 函数?

详细解答:

1. 基本结构

javascript 复制代码
// composables/useMouse.js
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. 接受响应式参数

javascript 复制代码
// composables/useFetch.js
import { ref, unref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  async function doFetch() {
    loading.value = true
    error.value = null
    
    try {
      // unref() 可以接受 ref 或普通值
      const urlValue = unref(url)
      const response = await fetch(urlValue)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  // 当 url 变化时重新请求
  watchEffect(() => {
    doFetch()
  })
  
  return { data, error, loading }
}

// 使用
const url = ref('/api/users')
const { data, error, loading } = useFetch(url)

// 也可以传入普通字符串
const { data } = useFetch('/api/posts')

3. 副作用清理

javascript 复制代码
// composables/useEventListener.js
import { onMounted, onUnmounted, watch } from 'vue'

export function useEventListener(target, event, callback, options) {
  onMounted(() => {
    const element = unref(target)
    element.addEventListener(event, callback, options)
  })
  
  onUnmounted(() => {
    const element = unref(target)
    element.removeEventListener(event, callback, options)
  })
}

// 使用
import { ref } from 'vue'
import { useEventListener } from './composables/useEventListener'

const buttonRef = ref(null)

useEventListener(buttonRef, 'click', () => {
  console.log('Button clicked')
})

// 也可以监听 window
useEventListener(window, 'resize', () => {
  console.log('Window resized')
})

4. 返回值的灵活性

javascript 复制代码
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  // 返回对象:命名导出
  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// 使用时可以重命名
const { count: myCount, increment: myIncrement } = useCounter(10)

5. 复杂 Composable 示例

javascript 复制代码
// composables/useAsync.js
import { ref, unref, watchEffect } from 'vue'

export function useAsync(asyncFn, options = {}) {
  const {
    immediate = true,
    initialData = null,
    onSuccess,
    onError,
    resetOnExecute = true
  } = options
  
  const data = ref(initialData)
  const error = ref(null)
  const loading = ref(false)
  const isReady = ref(false)
  
  async function execute(...args) {
    if (resetOnExecute) {
      data.value = initialData
    }
    
    error.value = null
    loading.value = true
    isReady.value = false
    
    try {
      const result = await asyncFn(...args)
      data.value = result
      isReady.value = true
      
      if (onSuccess) {
        onSuccess(result)
      }
      
      return result
    } catch (err) {
      error.value = err
      
      if (onError) {
        onError(err)
      }
      
      throw err
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    execute()
  }
  
  return {
    data,
    error,
    loading,
    isReady,
    execute
  }
}

// 使用
const { data: users, loading, error, execute: refetchUsers } = useAsync(
  () => fetch('/api/users').then(r => r.json()),
  {
    immediate: true,
    onSuccess: (users) => {
      console.log('Users loaded:', users.length)
    },
    onError: (error) => {
      console.error('Failed to load users:', error)
    }
  }
)

总结 - 组件通信方式选择:

场景 推荐方式 备选方案
父→子 Props Provide/Inject
子→父 Emits Refs
兄弟组件 状态管理(Pinia) Event Bus
跨层级 Provide/Inject 状态管理
全局状态 Pinia Composables
双向绑定 v-model Props + Emits
相关推荐
摇滚侠4 小时前
npm 设置了阿里云镜像,然后全局安装了 pnpm,pnpm 还需要设置阿里云镜像吗
前端·阿里云·npm
程序员清洒10 小时前
Flutter for OpenHarmony:GridView — 网格布局实现
android·前端·学习·flutter·华为
VX:Fegn089510 小时前
计算机毕业设计|基于ssm + vue超市管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
0思必得010 小时前
[Web自动化] 反爬虫
前端·爬虫·python·selenium·自动化
LawrenceLan10 小时前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart
秋秋小事10 小时前
TypeScript 模版字面量与类型操作
前端·typescript
2401_8920005211 小时前
Flutter for OpenHarmony 猫咪管家App实战 - 添加提醒实现
前端·javascript·flutter
Yolanda9411 小时前
【项目经验】vue h5移动端禁止缩放
前端·javascript·vue.js
VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue酒店管理系统(源码+数据库+文档)
vue.js·spring boot·课程设计