Vue 3 最常用的 20 道面试题总结

以下是关于 Vue 3 最常用的 20 道面试题总结,涵盖 Vue 3 的核心特性如 Composition API、响应式系统(ref/reactive)、生命周期钩子、组件通信等高频知识点。

1. Vue 3 和 Vue 2 的区别是什么?

问题: Vue 3 相比 Vue 2 有哪些重大改进?这些改进带来了什么好处?

答案:

Vue 3 在多个方面进行了重大升级,让开发体验和性能都有了质的飞跃:

  1. 响应式系统重写

    • Vue 2 使用 Object.defineProperty 实现响应式,存在无法检测数组/对象新增属性的问题
    • Vue 3 改用 Proxy 实现,完美解决了这些限制,还提升了性能
    • 实际开发中再也不用担心 this.$set 的问题了
  2. Composition API

    • 全新的 API 设计,让逻辑组织更灵活
    • 解决了 Vue 2 中 mixins 带来的命名冲突问题
    • 代码复用更方便,可以像搭积木一样组合功能
  3. 性能优化

    • 模块化架构支持 Tree-shaking,打包体积更小
    • 虚拟 DOM 重写,diff 算法更高效
    • 实测性能提升 2-3 倍
  4. TypeScript 支持

    • 原生支持 TS,类型推断更完善
    • 再也不用为类型定义发愁了
  5. 新特性

    • Fragment:组件可以有多个根节点
    • Teleport:轻松实现模态框等需要挂载到 body 的组件
    • Suspense:优雅处理异步组件加载状态
// 实际案例:体验 Composition API 的便利
import { ref, computed } from 'vue';

export default {
  setup() {
    // 响应式数据
    const count = ref(0);
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2);
    
    // 方法
    function increment() {
      count.value++;
    }
    
    return { count, doubleCount, increment };
  }
}

面试技巧:回答时可以结合自己的实际开发经验,比如:
“在我们项目中升级到 Vue 3 后,打包体积减少了 40%,而且 Composition API 让我们能更好地组织复杂组件的逻辑…”

// Vue 3 示例:使用 Composition API
import { ref, onMounted } from 'vue';

export default {
  setup() {
    const count = ref(0);
    function increment() {
      count.value++;
    }
    onMounted(() => {
      console.log('组件挂载');
    });
    return { count, increment };
  }
};

2. 如何在 Vue 3 中创建一个响应式对象?

问题: 在 Vue 3 中创建响应式对象有哪些方式?它们各有什么特点?

答案:

Vue 3 提供了两种主要方式来创建响应式数据:

  1. reactive() 方法
    • 专门用于创建响应式对象(Object/Array)
    • 返回一个 Proxy 代理对象
    • 修改属性时会自动触发视图更新
    • 适合管理复杂的状态对象
import { reactive } from 'vue';

// 创建响应式对象
const user = reactive({
  name: 'Alice',
  age: 25,
  address: {
    city: 'Shanghai'
  }
});

// 修改属性 - 会自动触发更新
user.name = 'Bob'; 
user.address.city = 'Beijing';

// 注意:解构会失去响应性!
const { name } = user; // ❌ 错误做法
  1. ref() 方法
    • 可以包装任何值类型(包括对象)
    • 通过 .value 访问和修改值
    • 适合管理单个值或需要保持引用的场景
import { ref } from 'vue';

const count = ref(0); // 基本类型
const userRef = ref({ name: 'Alice' }); // 也可以包装对象

// 修改值
count.value++; 
userRef.value.name = 'Bob';

实际开发建议

  • 对于表单数据、复杂状态对象优先使用 reactive()
  • 对于简单值、需要保持引用的场景使用 ref()
  • 使用 toRefs() 可以保持解构后的响应性:
    import { toRefs } from 'vue';
    const { name } = toRefs(user); // ✅ 正确解构方式
    name.value = 'Charlie';
    

面试技巧
可以结合项目经验说:“在我们项目中,用户信息这种复杂对象都用 reactive 管理,而像计数器、开关状态这种简单值用 ref 更合适。”

3. ref()reactive() 的深度对比

问题: 详细比较 ref()reactive() 的区别,并说明各自的最佳实践。

答案:

核心区别

特性 ref() reactive()
包装类型 任何值类型(基本/对象) 仅对象/数组
访问方式 通过 .value 访问 直接访问属性
模板中使用 自动解包(无需 .value 直接使用
响应式丢失 解构/传递时保持响应性 解构/传递会丢失响应性
替换整个对象 直接赋值新对象 需要使用 Object.assign
TypeScript 支持 类型推断更直接 嵌套对象类型需要额外处理

代码示例对比

import { ref, reactive } from 'vue';

// ref 示例
const count = ref(0); // 基本类型
const userRef = ref({ name: 'Alice' }); // 也可以包装对象

// 修改值
count.value++; 
userRef.value.name = 'Bob'; // 修改对象属性
userRef.value = { name: 'Charlie' }; // 替换整个对象

// reactive 示例
const user = reactive({ 
  name: 'Alice',
  profile: {
    age: 25
  }
});

// 修改属性
user.name = 'Bob';
user.profile.age = 26;

// ❌ 错误做法 - 会失去响应性
let { name } = user;
name = 'Charlie'; // 不会更新视图

// ✅ 正确做法 - 使用 toRefs
import { toRefs } from 'vue';
const { name } = toRefs(user);
name.value = 'Charlie'; // 保持响应性

最佳实践指南

  1. 何时使用 ref()

    • 管理基本类型值(string/number/boolean)
    • 需要替换整个对象引用时
    • 在组合函数中返回单个值时
    • 需要明确的值引用时
  2. 何时使用 reactive()

    • 管理表单数据对象
    • 复杂的状态对象
    • 嵌套层级深的数据结构
    • 需要直接访问属性的场景
  3. 性能考虑

    • 对于简单值,ref() 性能略优
    • 对于复杂对象,两者性能相当
    • 避免过度嵌套 ref() 对象
  4. 项目经验分享
    "在我们的后台管理系统中:

    • 表单数据全部使用 reactive() 管理
    • 全局状态中的简单标志位使用 ref()
    • 组件内部状态根据复杂度选择
    • 组合函数返回值优先使用 ref()"

常见陷阱及解决方案

  1. 响应式丢失问题

    // 错误 ❌
    const state = reactive({ count: 0 });
    return { ...state }; // 解构丢失响应性
    
    // 正确 ✅ 
    return { ...toRefs(state) }; // 保持响应性
    
  2. 类型推断问题

    // ref 类型推断
    const count = ref<number>(0); // 明确类型
    
    // reactive 类型推断
    interface User {
      name: string;
      age: number;
    }
    const user = reactive<User>({ name: '', age: 0 });
    
  3. 组合使用技巧

    // 在 reactive 中使用 ref
    const loading = ref(false);
    const state = reactive({
      loading, // 自动解包
      data: null
    });
    
    // 访问时
    state.loading = true; // 不需要 .value
    

4. Vue 3 响应式监听深度解析

问题: 详细比较 watchEffectwatch 的区别,并说明各自的最佳实践场景。

答案:

核心区别对比

特性 watchEffect watch
依赖收集 自动收集所有依赖 需要显式指定监听源
立即执行 立即执行 默认不立即执行(可配置)
参数获取 无法获取旧值 可以获取新旧值
性能优化 适合简单副作用 适合精确控制监听
停止监听 返回停止函数 返回停止函数
调试 较难调试 更容易调试

代码示例对比

import { ref, watchEffect, watch } from 'vue';

const count = ref(0);
const user = reactive({ name: 'Alice', age: 25 });

// watchEffect 基础用法
const stopEffect = watchEffect(() => {
  console.log('自动追踪的依赖:', count.value, user.name);
  // 副作用操作,如API调用
});

// watch 基础用法
const stopWatch = watch(
  // 监听源
  [count, () => user.age], 
  // 回调函数
  ([newCount, newAge], [oldCount, oldAge]) => {
    console.log(`Count变化: ${oldCount} -> ${newCount}`);
    console.log(`Age变化: ${oldAge} -> ${newAge}`);
  },
  // 选项
  { immediate: true, deep: true }
);

// 停止监听
stopEffect();
stopWatch();

最佳实践指南

  1. 何时使用 watchEffect

    • 需要自动追踪多个依赖时
    • 执行不依赖旧值的副作用(如日志、API调用)
    • 简单的响应式操作(DOM操作、事件监听)
    • 组合式函数中的副作用管理
  2. 何时使用 watch

    • 需要比较新旧值时
    • 需要精确控制监听源时
    • 性能敏感场景(避免不必要的触发)
    • 需要延迟执行或条件执行时
  3. 性能优化技巧

    // 优化前 - 每次user变化都会执行
    watchEffect(() => {
      if (user.age > 18) {
        console.log('Adult');
      }
    });
    
    // 优化后 - 只有age变化时执行
    watch(
      () => user.age,
      (age) => {
        if (age > 18) {
          console.log('Adult');
        }
      }
    );
    
  4. 项目经验分享
    "在我们的电商项目中:

    • 表单自动保存使用 watchEffect 自动追踪所有字段
    • 价格计算使用 watch 精确监听价格相关字段
    • 路由参数变化使用 watch 来获取新旧路由信息
    • 全局状态监听使用 watchEffect 简化代码"

高级用法示例

  1. 异步操作处理

    // 使用 watchEffect 处理异步
    watchEffect(async (onCleanup) => {
      const { data } = await fetchUser(count.value);
      onCleanup(() => {
        // 取消未完成的请求
      });
    });
    
    // 使用 watch 处理异步
    watch(
      count,
      async (newVal, oldVal, onCleanup) => {
        const { data } = await fetchUser(newVal);
        onCleanup(() => {
          // 清理逻辑
        });
      }
    );
    
  2. 调试技巧

    watch(
      count,
      (newVal, oldVal) => {
        debugger; // 更容易调试
      },
      { flush: 'post' } // DOM更新后触发
    );
    
  3. 性能敏感场景

    // 节流监听
    watch(
      count,
      _.throttle((newVal) => {
        console.log('节流输出:', newVal);
      }, 1000)
    );
    

常见问题解决方案

  1. 无限循环问题

    // 错误 ❌ - 会导致无限循环
    watchEffect(() => {
      user.age++; 
    });
    
    // 正确 ✅ - 添加条件判断
    watchEffect(() => {
      if (user.age < 100) {
        user.age++;
      }
    });
    
  2. DOM 操作时机

    // 确保DOM更新后执行
    watchEffect(() => {
      nextTick(() => {
        // DOM操作
      });
    }, { flush: 'post' });
    
  3. TypeScript 类型支持

    // watch 类型推断
    watch<number>(
      count,
      (newVal: number) => {
        // 明确类型
      }
    );
    
    // watchEffect 类型
    watchEffect((onCleanup) => {
      onCleanup(() => {
        // 清理函数
      });
    });
    

5. Vue 3 生命周期全面解析

问题: 详细讲解 Vue 3 的生命周期钩子,包括 Composition API 和 Options API 的对比,以及实际应用场景。

答案:

生命周期钩子对比表

Options API Composition API 触发时机 常用场景
beforeCreate - 实例初始化前 极少使用
created - 实例创建完成 初始化非响应式数据
beforeMount onBeforeMount 挂载开始前 极少使用
mounted onMounted 挂载完成后 DOM 操作、API 调用
beforeUpdate onBeforeUpdate 数据变化,DOM 更新前 获取更新前的 DOM 状态
updated onUpdated 数据变化,DOM 更新后 DOM 依赖操作(谨慎使用)
beforeUnmount onBeforeUnmount 实例销毁前 清理定时器、事件监听
unmounted onUnmounted 实例销毁后 清理内存、取消请求
errorCaptured onErrorCaptured 捕获后代组件错误 错误处理
renderTracked onRenderTracked 渲染依赖被追踪时 调试
renderTriggered onRenderTriggered 依赖触发重新渲染时 调试
activated onActivated <keep-alive> 组件激活时 恢复组件状态
deactivated onDeactivated <keep-alive> 组件停用时 保存组件状态

Composition API 使用示例

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated
} from 'vue';

export default {
  setup() {
    // 组件挂载阶段
    onBeforeMount(() => {
      console.log('DOM 挂载前');
    });

    onMounted(() => {
      console.log('DOM 挂载完成');
      // 实际应用:初始化图表、地图等第三方库
      initChart();
    });

    // 组件更新阶段
    onBeforeUpdate(() => {
      console.log('数据变化,DOM 更新前');
    });

    onUpdated(() => {
      console.log('数据变化,DOM 更新后');
      // 实际应用:更新第三方库
      updateChart();
    });

    // 组件卸载阶段
    onBeforeUnmount(() => {
      console.log('组件卸载前');
      // 实际应用:清除定时器
      clearInterval(timer);
    });

    onUnmounted(() => {
      console.log('组件卸载完成');
      // 实际应用:销毁第三方库实例
      destroyChart();
    });

    // KeepAlive 相关
    onActivated(() => {
      console.log('组件被激活');
      // 实际应用:恢复播放器状态
      video.play();
    });

    onDeactivated(() => {
      console.log('组件被停用');
      // 实际应用:暂停播放器
      video.pause();
    });
  }
};

最佳实践指南

  1. 初始化数据

    • 使用 setup() 替代 createdbeforeCreate
    • 异步数据请求应在 onMounted 中进行
  2. DOM 操作

    onMounted(() => {
      // 正确 ✅ DOM 已挂载
      const el = document.getElementById('my-element');
    });
    
  3. 清理资源

    const timer = ref(null);
    
    onMounted(() => {
      timer.value = setInterval(() => {
        // 定时任务
      }, 1000);
    });
    
    onUnmounted(() => {
      clearInterval(timer.value);
    });
    
  4. 性能优化

    • 避免在 updated 中修改状态(可能导致无限循环)
    • 对于复杂计算,使用 watch 替代 updated
  5. 错误处理

    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err);
      // 可以阻止错误继续向上传播
      return false;
    });
    

项目经验分享

"在我们的管理后台项目中:

  • 使用 onMounted 初始化 ECharts 图表
  • 使用 onUnmounted 清理 WebSocket 连接
  • 使用 onActivated/onDeactivated 保存和恢复表单状态
  • 使用 onErrorCaptured 统一处理组件错误"

常见问题解决方案

  1. 异步组件生命周期

    // 父组件
    <Suspense>
      <AsyncComponent />
    </Suspense>
    
    // AsyncComponent
    onMounted(() => {
      // 会在组件真正挂载后执行
    });
    
  2. 生命周期执行顺序

    // 父子组件生命周期顺序
    Parent onBeforeMount
    Child onBeforeMount
    Child onMounted 
    Parent onMounted
    
  3. 调试技巧

    onRenderTracked((e) => {
      console.log('依赖被追踪:', e);
    });
    
    onRenderTriggered((e) => {
      console.log('依赖触发更新:', e);
    });
    
  4. TypeScript 类型支持

    import type { Ref } from 'vue';
    
    onMounted(() => {
      const count: Ref<number> = ref(0);
    });
    

高级用法示例

  1. 组合函数中的生命周期

    // useFetch.js
    export function useFetch(url) {
      const data = ref(null);
      
      onMounted(async () => {
        data.value = await fetch(url).then(r => r.json());
      });
      
      return { data };
    }
    
    // 组件中使用
    export default {
      setup() {
        const { data } = useFetch('/api/data');
        return { data };
      }
    };
    
  2. 动态组件生命周期

    const dynamicComponent = markRaw({
      setup() {
        onMounted(() => {
          console.log('动态组件挂载');
        });
        return () => h('div', '动态组件');
      }
    });
    
  3. 服务端渲染注意事项

    onMounted(() => {
      // 只在客户端执行
      if (process.client) {
        initClientOnlyLibrary();
      }
    });
    

6. Vue 3 组件通信全面指南

问题: 详细讲解 Vue 3 中各种组件通信方式,包括父子组件、兄弟组件、跨层级组件等场景的最佳实践。

答案:

组件通信方式对比表

方式 适用场景 优点 缺点
Props/Emits 父子组件通信 简单直接,Vue 原生支持 不适合深层嵌套组件
v-model 父子组件双向绑定 语法简洁,符合 Vue 习惯 只能绑定单个值
Refs 父组件访问子组件实例 直接访问组件方法和数据 破坏封装性,应谨慎使用
Provide/Inject 跨层级组件通信 解决深层嵌套通信问题 数据流向不直观
Event Bus 任意组件间通信 灵活,不受组件关系限制 难以追踪,大型项目慎用
Pinia/Vuex 全局状态管理 集中管理,适合复杂应用 小型项目可能过度设计
Slot 内容分发 灵活定制组件内容 不适合数据通信
Attrs/Listeners 透传属性和事件 方便高阶组件开发 需要明确文档说明

1. Props/Emits (父子通信)

基础用法

// 子组件
export default {
  props: {
    title: {
      type: String,
      required: true,
      default: '默认标题'
    },
    count: Number
  },
  emits: ['update', 'delete'],
  setup(props, { emit }) {
    const handleClick = () => {
      emit('update', { newValue: '更新值' });
    };
    return { handleClick };
  }
};

// 父组件
<template>
  <Child 
    :title="pageTitle" 
    :count="pageCount"
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

最佳实践

  1. 始终为 props 定义类型和验证
  2. 复杂对象使用 reactive 包装
  3. 使用 emits 选项明确声明事件
  4. 事件命名使用 kebab-case (如 update-count)

2. v-model 双向绑定

基础用法

// 子组件
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const updateValue = (e) => {
      emit('update:modelValue', e.target.value);
    };
    return { updateValue };
  }
};

// 父组件
<CustomInput v-model="searchText" />

多 v-model 绑定

// 子组件
export default {
  props: ['firstName', 'lastName'],
  emits: ['update:firstName', 'update:lastName']
};

// 父组件
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

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

基础用法

// 祖先组件
import { provide, ref } from 'vue';

export default {
  setup() {
    const theme = ref('dark');
    provide('theme', {
      theme,
      toggleTheme: () => {
        theme.value = theme.value === 'dark' ? 'light' : 'dark';
      }
    });
  }
};

// 后代组件
import { inject } from 'vue';

export default {
  setup() {
    const { theme, toggleTheme } = inject('theme');
    return { theme, toggleTheme };
  }
};

最佳实践

  1. 为注入值提供默认值
  2. 使用 Symbol 作为注入名避免冲突
  3. 考虑使用响应式对象封装相关功能

4. 模板引用 (Refs)

基础用法

// 父组件
<template>
  <ChildComponent ref="childRef" />
</template>

<script>
import { ref, onMounted } from 'vue';

export default {
  setup() {
    const childRef = ref(null);
    
    onMounted(() => {
      // 访问子组件实例
      console.log(childRef.value.someMethod());
    });
    
    return { childRef };
  }
};
</script>

// 子组件需使用 defineExpose 暴露方法
export default {
  setup() {
    const someMethod = () => 'Hello';
    defineExpose({ someMethod });
  }
};

5. 事件总线 (Event Bus)

实现方案

// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();

// 组件A (发送事件)
import { emitter } from './eventBus';
emitter.emit('user-logged-in', { userId: 123 });

// 组件B (监听事件)
import { emitter } from './eventBus';
import { onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const handleLogin = (user) => {
      console.log('User logged in:', user);
    };
    
    onMounted(() => {
      emitter.on('user-logged-in', handleLogin);
    });
    
    onUnmounted(() => {
      emitter.off('user-logged-in', handleLogin);
    });
  }
};

6. Pinia/Vuex (状态管理)

Pinia 示例

// store/user.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    name: 'Guest',
    isAdmin: false
  }),
  actions: {
    login(user) {
      this.name = user.name;
      this.isAdmin = user.isAdmin;
    }
  }
});

// 组件中使用
import { useUserStore } from '@/stores/user';

export default {
  setup() {
    const userStore = useUserStore();
    const login = () => {
      userStore.login({ name: 'Admin', isAdmin: true });
    };
    return { userStore, login };
  }
};

7. 作用域插槽 (Scoped Slots)

高级用法

<!-- 子组件 -->
<template>
  <slot name="item" v-bind="{ value, index }"></slot>
</template>

<script>
export default {
  props: ['items'],
  setup(props) {
    return { items: props.items };
  }
};
</script>

<!-- 父组件 -->
<template>
  <List :items="users">
    <template #item="{ value, index }">
      {{ index + 1 }}. {{ value.name }}
    </template>
  </List>
</template>

项目实战经验

"在我们的电商项目中:

  1. 父子组件使用 props/emits 处理表单数据
  2. 主题切换使用 provide/inject 实现全局主题
  3. 购物车使用 Pinia 管理全局状态
  4. 复杂表单使用 v-model 简化双向绑定
  5. 通知系统使用事件总线实现跨组件通信"

常见问题解决方案

  1. Props 数据流问题

    // 子组件需要修改 prop 时
    const localValue = ref(props.modelValue);
    watch(() => props.modelValue, (newVal) => {
      localValue.value = newVal;
    });
    watch(localValue, (newVal) => {
      emit('update:modelValue', newVal);
    });
    
  2. 跨组件通信性能优化

    // 使用 shallowRef 避免不必要的响应式
    provide('largeData', shallowRef(largeDataSet));
    
  3. TypeScript 类型支持

    // 为 provide/inject 提供类型
    interface ThemeContext {
      theme: Ref<string>;
      toggleTheme: () => void;
    }
    
    const themeContext = inject<ThemeContext>('theme');
    
  4. 动态组件通信

    // 使用 markRaw 避免不必要的响应式
    const tabs = ref([
      { component: markRaw(ComponentA), name: 'A' },
      { component: markRaw(ComponentB), name: 'B' }
    ]);
    

高级模式示例

  1. 依赖注入模式

    // service.js
    export class DataService {
      fetchData() {
        return Promise.resolve('data');
      }
    }
    
    // 根组件
    import { DataService } from './service';
    provide('dataService', new DataService());
    
    // 子组件
    const dataService = inject('dataService');
    dataService.fetchData().then(console.log);
    
  2. 基于 Symbol 的注入键

    // keys.js
    export const THEME_KEY = Symbol('theme');
    
    // 提供者
    provide(THEME_KEY, theme);
    
    // 消费者
    const theme = inject(THEME_KEY);
    
  3. 组合式函数通信

    // useCounter.js
    export function useCounter() {
      const count = ref(0);
      const increment = () => count.value++;
      return { count, increment };
    }
    
    // 多个组件共享同一状态
    const { count, increment } = useCounter();
    

7. Vue 3 的 setup() 函数深度解析

问题: 全面解析 setup() 函数的设计原理、核心功能和使用技巧。

答案:

1. setup() 的核心作用

setup() 是 Composition API 的入口点,它:

  • 替代了 Vue 2 的 data, methods, computed 等选项
  • 提供了更灵活的逻辑组织和复用方式
  • 在组件创建之前执行,没有 this 上下文

2. 基本用法

import { ref } from 'vue';

export default {
  setup(props, context) {
    // 响应式数据
    const count = ref(0);
    
    // 方法
    const increment = () => {
      count.value++;
    };
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2);
    
    // 生命周期钩子
    onMounted(() => {
      console.log('组件已挂载');
    });
    
    // 返回模板可用的内容
    return {
      count,
      increment,
      doubleCount
    };
  }
};

3. 参数详解

setup() 接收两个参数:

  1. props - 组件接收的 props

    setup(props) {
      // props 是响应式的
      watch(() => props.id, (newVal) => {
        fetchData(newVal);
      });
    }
    
  2. context - 包含常用属性/方法:

    setup(props, { attrs, slots, emit, expose }) {
      // attrs - 非 props 的属性
      // slots - 插槽内容
      // emit - 触发事件
      // expose - 暴露公共属性
    }
    

4. 与 Options API 对比

特性 Options API Composition API (setup)
组织方式 按选项类型分组 按逻辑功能组织
代码复用 Mixins/Scoped Slots 组合函数
TypeScript 支持有限 完整类型支持
this 上下文 可用 不可用
灵活性 较低 更高

5. 最佳实践

  1. 逻辑提取

    // useCounter.js
    export function useCounter(initialValue = 0) {
      const count = ref(initialValue);
      const increment = () => count.value++;
      return { count, increment };
    }
    
    // 组件中使用
    export default {
      setup() {
        const { count, increment } = useCounter();
        return { count, increment };
      }
    };
    
  2. Props 处理

    export default {
      props: {
        userId: {
          type: Number,
          required: true
        }
      },
      setup(props) {
        // 使用 toRef 保持响应性
        const userId = toRef(props, 'userId');
        
        watch(userId, (newVal) => {
          fetchUser(newVal);
        });
      }
    };
    
  3. 上下文使用

    export default {
      setup(props, { emit }) {
        const submitForm = () => {
          emit('submit', formData);
        };
        
        return { submitForm };
      }
    };
    

6. <script setup> 语法糖

更简洁的写法:

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

const count = ref(0);
const increment = () => count.value++;
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

7. 项目实战经验

"在我们的管理后台项目中:

  1. 复杂表单使用 setup() 组织验证逻辑
  2. 数据获取封装成组合函数复用
  3. 使用 <script setup> 简化代码
  4. 类型推断使 TypeScript 支持更完善"

8. 常见问题解决方案

  1. 响应式丢失

    // 错误 ❌
    return { ...state };
    
    // 正确 ✅
    return { ...toRefs(state) };
    
  2. 异步组件

    setup() {
      const data = ref(null);
      
      onMounted(async () => {
        data.value = await fetchData();
      });
      
      return { data };
    }
    
  3. TypeScript 类型

    interface Props {
      id: number;
      title?: string;
    }
    
    export default {
      props: {
        id: { type: Number, required: true },
        title: { type: String, default: '' }
      },
      setup(props: Props) {
        // 类型安全的 props
      }
    };
    

9. 高级用法示例

  1. 依赖注入

    // 提供者
    const theme = ref('light');
    provide('theme', theme);
    
    // 消费者
    const theme = inject('theme');
    
  2. 模板引用

    <template>
      <input ref="inputRef">
    </template>
    
    <script setup>
    import { ref, onMounted } from 'vue';
    
    const inputRef = ref(null);
    
    onMounted(() => {
      inputRef.value.focus();
    });
    </script>
    
  3. 动态组件

    const currentComponent = ref('ComponentA');
    
    const components = {
      ComponentA,
      ComponentB
    };
    
  4. 性能优化

    // 使用 shallowRef 避免深度响应式
    const largeObject = shallowRef({ ... });
    
  5. SSR 兼容

    onMounted(() => {
      // 只在客户端执行
      if (process.client) {
        initClientLibrary();
      }
    });
    

8. Vue 3 响应式监听深度解析

问题: 详细比较 watchEffectwatch 的区别,并说明各自的最佳实践场景。

答案:

核心区别对比

特性 watchEffect watch
依赖收集 自动收集所有依赖 需要显式指定监听源
立即执行 立即执行 默认不立即执行(可配置)
参数获取 无法获取旧值 可以获取新旧值
性能优化 适合简单副作用 适合精确控制监听
停止监听 返回停止函数 返回停止函数
调试 较难调试 更容易调试

代码示例对比

import { ref, watchEffect, watch } from 'vue';

const count = ref(0);
const user = reactive({ name: 'Alice', age: 25 });

// watchEffect 基础用法
const stopEffect = watchEffect(() => {
  console.log('自动追踪的依赖:', count.value, user.name);
  // 副作用操作,如API调用
});

// watch 基础用法
const stopWatch = watch(
  // 监听源
  [count, () => user.age], 
  // 回调函数
  ([newCount, newAge], [oldCount, oldAge]) => {
    console.log(`Count变化: ${oldCount} -> ${newCount}`);
    console.log(`Age变化: ${oldAge} -> ${newAge}`);
  },
  // 选项
  { immediate: true, deep: true }
);

// 停止监听
stopEffect();
stopWatch();

最佳实践指南

  1. 何时使用 watchEffect

    • 需要自动追踪多个依赖时
    • 执行不依赖旧值的副作用(如日志、API调用)
    • 简单的响应式操作(DOM操作、事件监听)
    • 组合式函数中的副作用管理
  2. 何时使用 watch

    • 需要比较新旧值时
    • 需要精确控制监听源时
    • 性能敏感场景(避免不必要的触发)
    • 需要延迟执行或条件执行时
  3. 性能优化技巧

    // 优化前 - 每次user变化都会执行
    watchEffect(() => {
      if (user.age > 18) {
        console.log('Adult');
      }
    });
    
    // 优化后 - 只有age变化时执行
    watch(
      () => user.age,
      (age) => {
        if (age > 18) {
          console.log('Adult');
        }
      }
    );
    
  4. 项目经验分享
    "在我们的电商项目中:

    • 表单自动保存使用 watchEffect 自动追踪所有字段
    • 价格计算使用 watch 精确监听价格相关字段
    • 路由参数变化使用 watch 来获取新旧路由信息
    • 全局状态监听使用 watchEffect 简化代码"

高级用法示例

  1. 异步操作处理

    // 使用 watchEffect 处理异步
    watchEffect(async (onCleanup) => {
      const { data } = await fetchUser(count.value);
      onCleanup(() => {
        // 取消未完成的请求
      });
    });
    
    // 使用 watch 处理异步
    watch(
      count,
      async (newVal, oldVal, onCleanup) => {
        const { data } = await fetchUser(newVal);
        onCleanup(() => {
          // 清理逻辑
        });
      }
    );
    
  2. 调试技巧

    watch(
      count,
      (newVal, oldVal) => {
        debugger; // 更容易调试
      },
      { flush: 'post' } // DOM更新后触发
    );
    
  3. 性能敏感场景

    // 节流监听
    watch(
      count,
      _.throttle((newVal) => {
        console.log('节流输出:', newVal);
      }, 1000)
    );
    

常见问题解决方案

  1. 无限循环问题

    // 错误 ❌ - 会导致无限循环
    watchEffect(() => {
      user.age++; 
    });
    
    // 正确 ✅ - 添加条件判断
    watchEffect(() => {
      if (user.age < 100) {
        user.age++;
      }
    });
    
  2. DOM 操作时机

    // 确保DOM更新后执行
    watchEffect(() => {
      nextTick(() => {
        // DOM操作
      });
    }, { flush: 'post' });
    
  3. TypeScript 类型支持

    // watch 类型推断
    watch<number>(
      count,
      (newVal: number) => {
        // 明确类型
      }
    );
    
    // watchEffect 类型
    watchEffect((onCleanup) => {
      onCleanup(() => {
        // 清理函数
      });
    });
    

9. Vue 3 依赖注入深度解析

问题: 全面解析 provideinject 的工作原理、最佳实践和高级用法。

答案:

1. 核心概念

provideinject 实现了 Vue 的依赖注入系统:

  • provide: 在祖先组件中提供数据/方法
  • inject: 在后代组件中注入依赖项
  • 解决了 props 逐层传递的问题(prop drilling)

2. 基础用法

// 提供者组件 (祖先)
import { provide, ref } from 'vue';

export default {
  setup() {
    // 提供响应式数据
    const theme = ref('dark');
    provide('theme', theme);
    
    // 提供方法
    const toggleTheme = () => {
      theme.value = theme.value === 'dark' ? 'light' : 'dark';
    };
    provide('toggleTheme', toggleTheme);
    
    return { theme, toggleTheme };
  }
};

// 消费者组件 (后代)
import { inject } from 'vue';

export default {
  setup() {
    // 注入依赖项
    const theme = inject('theme');
    const toggleTheme = inject('toggleTheme');
    
    // 提供默认值
    const fontSize = inject('fontSize', ref('16px'));
    
    return { theme, toggleTheme, fontSize };
  }
};

3. 最佳实践指南

  1. 命名规范

    • 使用有意义的注入键名
    • 推荐使用 Symbol 避免命名冲突:
      // keys.js
      export const THEME_KEY = Symbol('theme');
      
      // 提供者
      provide(THEME_KEY, theme);
      
      // 消费者
      const theme = inject(THEME_KEY);
      
  2. 响应式处理

    // 提供响应式对象
    const state = reactive({ theme: 'dark' });
    provide('state', readonly(state)); // 使用 readonly 防止意外修改
    
  3. 组合式函数封装

    // useTheme.js
    export function useTheme() {
      const theme = inject(THEME_KEY);
      if (!theme) {
        throw new Error('必须在 ThemeProvider 下使用');
      }
      return theme;
    }
    
    // 组件中使用
    const theme = useTheme();
    
  4. 类型安全 (TypeScript)

    interface ThemeContext {
      theme: Ref<string>;
      toggleTheme: () => void;
    }
    
    // 提供者
    provide<ThemeContext>('theme', { theme, toggleTheme });
    
    // 消费者
    const { theme, toggleTheme } = inject<ThemeContext>('theme')!;
    

4. 项目实战经验

"在我们的后台管理系统中:

  1. 使用 provide/inject 实现全局主题切换
  2. 表单上下文通过注入共享验证规则
  3. 多层级菜单组件共享选中状态
  4. 用户权限信息通过注入传递"

5. 高级用法示例

  1. 依赖注入模式

    // authProvider.js
    export function provideAuth() {
      const user = ref(null);
      const login = () => { /* ... */ };
      const logout = () => { /* ... */ };
      
      provide('auth', { user, login, logout });
    }
    
    // useAuth.js
    export function useAuth() {
      const auth = inject('auth');
      if (!auth) {
        throw new Error('必须在 AuthProvider 下使用');
      }
      return auth;
    }
    
  2. 动态注入

    // 动态提供不同实现
    provide('api', isAdmin ? adminApi : userApi);
    
  3. 多层注入

    // 中间层组件可以覆盖注入
    provide('config', { ...inject('config'), theme: 'custom' });
    
  4. SSR 兼容

    // 服务端渲染时安全使用
    const theme = inject('theme', () => ref('light'), true);
    

6. 常见问题解决方案

  1. 注入未提供

    // 提供默认值
    const config = inject('config', { theme: 'light' });
    
    // 或抛出错误
    const config = inject('config');
    if (!config) {
      throw new Error('必须提供 config');
    }
    
  2. 响应式丢失

    // 错误 ❌
    provide('user', { ...user });
    
    // 正确 ✅
    provide('user', toRefs(user));
    
  3. 循环依赖

    // 使用工厂函数延迟解析
    provide('service', () => new Service(inject('config')));
    
  4. 性能优化

    // 大对象使用 shallowRef
    provide('largeData', shallowRef(bigData));
    

7. 与 Vuex/Pinia 对比

特性 Provide/Inject Vuex/Pinia
适用范围 组件层级关系明确 全局状态管理
组织方式 分散在各组件 集中式存储
调试工具 有专门的调试工具
TypeScript 需要额外处理 原生支持良好
性能 更适合局部状态 适合全局频繁更新的状态
学习曲线 简单 需要学习概念

8. 设计模式应用

  1. 策略模式

    // 提供不同策略实现
    provide('validation', {
      email: (value) => /@/.test(value),
      password: (value) => value.length >= 8
    });
    
  2. 观察者模式

    // 提供事件总线
    const listeners = new Set();
    provide('events', {
      on: (fn) => listeners.add(fn),
      emit: (data) => listeners.forEach(fn => fn(data))
    });
    
  3. 工厂模式

    // 提供组件工厂
    provide('componentFactory', (type) => {
      switch(type) {
        case 'A': return ComponentA;
        case 'B': return ComponentB;
      }
    });
    

9. 单元测试策略

  1. 测试提供者

    test('should provide theme', () => {
      const wrapper = mount(ThemeProvider);
      expect(wrapper.vm.theme).toBe('dark');
    });
    
  2. 测试消费者

    test('should inject theme', () => {
      const wrapper = mount(Consumer, {
        global: {
          provide: {
            theme: { value: 'test' }
          }
        }
      });
      expect(wrapper.vm.theme).toBe('test');
    });
    
  3. 测试默认值

    test('should use default value', () => {
      const wrapper = mount(Consumer);
      expect(wrapper.vm.fontSize).toBe('16px');
    });
    

10. 性能优化技巧

  1. 记忆化计算

    provide('filteredList', computed(() => 
      bigList.value.filter(item => item.active)
    ));
    
  2. 部分提供

    // 只提供必要数据
    provide('user', { name: user.name });
    
  3. 懒加载

    provide('heavyData', () => loadHeavyData());
    
  4. 作用域隔离

    // 使用 Symbol 避免冲突
    provide(Symbol('user'), user);
    

10. Vue 3 中如何使用插槽(Slot)?

问题: 实现一个默认插槽和具名插槽。

答案:

<!-- 父组件 -->
<template>
  <Card>
    <template #default>这是默认插槽内容</template>
    <template #header>这是头部插槽</template>
  </Card>
</template>

<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <header><slot name="header"></slot></header>
    <main><slot></slot></main>
  </div>
</template>

11. Vue 3 Teleport 组件深度解析

问题: 全面解析 Teleport 的工作原理、使用场景和最佳实践。

答案:

1. Teleport 的核心作用

Teleport 是 Vue 3 新增的内置组件,它:

  • 允许将组件内容渲染到 DOM 中的任意位置
  • 解决 z-index、overflow 等 CSS 层级问题
  • 保持组件逻辑和模板的完整性
  • 支持动态目标位置切换

2. 基础用法

<template>
  <button @click="showModal = true">打开模态框</button>
  
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <h2>模态框标题</h2>
      <p>模态框内容...</p>
      <button @click="showModal = false">关闭</button>
    </div>
  </Teleport>
</template>

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

const showModal = ref(false);
</script>

<style scoped>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 20px;
  z-index: 1000;
}
</style>

3. 最佳实践指南

  1. 常见使用场景

    • 模态框/对话框
    • 通知/提示框
    • 下拉菜单
    • 工具提示
    • 全屏加载动画
  2. 动态目标位置

    <Teleport :to="targetElement">
      <div v-if="show">动态内容</div>
    </Teleport>
    
    <script setup>
    const targetElement = ref('body');
    // 可以动态改变目标位置
    function changeTarget() {
      targetElement.value = '#custom-container';
    }
    </script>
    
  3. 多个 Teleport 合并

    <!-- 相同目标的多Teleport会按顺序合并 -->
    <Teleport to="#modals">
      <div>第一个内容</div>
    </Teleport>
    
    <Teleport to="#modals">
      <div>第二个内容</div>
    </Teleport>
    
    <!-- 渲染结果 -->
    <div id="modals">
      <div>第一个内容</div>
      <div>第二个内容</div>
    </div>
    
  4. 与 Transition 结合

    <Teleport to="body">
      <Transition name="fade">
        <div v-if="show" class="modal">...</div>
      </Transition>
    </Teleport>
    

4. 项目实战经验

"在我们的管理后台项目中:

  1. 全局通知组件使用 Teleport 渲染到 body 末尾
  2. 模态框避免被父组件 overflow:hidden 影响
  3. 动态表单的复杂弹窗使用 Teleport 管理层级
  4. 多步骤向导在不同容器间切换展示"

5. 高级用法示例

  1. SSR 兼容

    <!-- 仅在客户端渲染 -->
    <ClientOnly>
      <Teleport to="body">
        <ClientSideComponent />
      </Teleport>
    </ClientOnly>
    
  2. 可访问性增强

    <Teleport to="body">
      <div 
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
      >
        <h2 id="modal-title">可访问的模态框</h2>
      </div>
    </Teleport>
    
  3. 与组合式函数结合

    // useModal.js
    export function useModal() {
      const isOpen = ref(false);
      
      const Modal = {
        setup(props, { slots }) {
          return () => h(
            Teleport,
            { to: 'body' },
            [slots.default?.()]
          );
        }
      };
      
      return { isOpen, Modal };
    }
    
    // 使用
    const { isOpen, Modal } = useModal();
    

6. 常见问题解决方案

  1. 目标元素不存在

    // 确保目标元素存在
    onMounted(() => {
      if (!document.getElementById('modal-container')) {
        const el = document.createElement('div');
        el.id = 'modal-container';
        document.body.appendChild(el);
      }
    });
    
  2. SSR 报错

    // 检查是否在客户端
    const isClient = typeof window !== 'undefined';
    
  3. 动画过渡问题

    <Teleport to="body">
      <Transition name="fade" mode="out-in">
        <div v-if="show">...</div>
      </Transition>
    </Teleport>
    
  4. TypeScript 支持

    const target = ref<HTMLElement | string>('body');
    

7. 性能优化技巧

  1. 延迟加载

    <Teleport to="body">
      <div v-if="show" v-memo="[show]">...</div>
    </Teleport>
    
  2. 复用 DOM

    // 使用 keep-alive 保留状态
    <Teleport to="body">
      <KeepAlive>
        <Component :is="currentComponent" />
      </KeepAlive>
    </Teleport>
    
  3. 虚拟滚动集成

    <Teleport to="body">
      <VirtualList :items="largeData">
        <!-- 列表项 -->
      </VirtualList>
    </Teleport>
    

8. 设计模式应用

  1. 门户模式

    // 创建全局门户管理器
    const portalTargets = reactive({
      topBar: null,
      sidebar: null
    });
    
    provide('portalTargets', portalTargets);
    
  2. 策略模式

    const portalStrategy = {
      modal: 'body',
      tooltip: '#tooltip-container',
      default: '#app'
    };
    
    <Teleport :to="portalStrategy[type]">
      <!-- 内容 -->
    </Teleport>
    
  3. 工厂模式

    function createPortalComponent(target) {
      return {
        setup(_, { slots }) {
          return () => h(Teleport, { to: target }, slots.default?.());
        }
      };
    }
    

9. 单元测试策略

  1. 测试 Teleport 内容

    test('renders content in target', async () => {
      const wrapper = mount(Component);
      await wrapper.find('button').trigger('click');
      expect(document.body.innerHTML).toContain('Modal Content');
    });
    
  2. 测试动态目标

    test('changes target dynamically', async () => {
      const wrapper = mount(Component);
      wrapper.vm.target = '#new-target';
      await nextTick();
      expect(document.querySelector('#new-target')).toBeTruthy();
    });
    
  3. 测试可访问性

    test('has proper ARIA attributes', () => {
      const wrapper = mount(Component);
      expect(wrapper.find('[role="dialog"]').exists()).toBe(true);
    });
    

10. 与 Vue 2 的对比

特性 Vue 2 (portal-vue) Vue 3 Teleport
实现方式 第三方库 内置组件
性能 额外 DOM 操作 原生支持,性能更优
API 需要单独安装 开箱即用
SSR 支持 需要额外配置 更好支持
TypeScript 类型定义可能不全 完整类型支持
动态目标 支持 原生支持
多个实例 需要手动管理 自动合并
过渡动画 需要额外处理 与 Transition 无缝集成

12. Vue 3 Suspense 组件深度解析

问题: 全面解析 Suspense 的工作原理、使用场景和最佳实践。

答案:

1. Suspense 的核心作用

Suspense 是 Vue 3 新增的内置组件,它:

  • 优雅处理异步组件加载状态
  • 统一管理异步操作的加载、错误和完成状态
  • 支持嵌套的异步依赖
  • 与 Composition API 深度集成

2. 基础用法

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading-spinner">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => 
  import('./MyComponent.vue')
);
</script>

<style>
.loading-spinner {
  /* 加载动画样式 */
}
</style>

3. 最佳实践指南

  1. 异步组件定义

    // 带配置的异步组件
    const AsyncComponent = defineAsyncComponent({
      loader: () => import('./MyComponent.vue'),
      delay: 200, // 延迟显示加载状态
      timeout: 3000, // 超时时间
      suspensible: false, // 是否由 Suspense 控制
      onError(error, retry, fail) {
        // 错误处理
      }
    });
    
  2. 组合式 API 集成

    // 使用 async setup
    const AsyncComponent = defineAsyncComponent({
      async setup() {
        const data = await fetchData();
        return () => h('div', data);
      }
    });
    
  3. 错误处理

    <Suspense>
      <template #default>
        <AsyncComponent />
      </template>
      <template #fallback>
        <Loading />
      </template>
      <template #error>
        <ErrorDisplay />
      </template>
    </Suspense>
    
  4. 嵌套 Suspense

    <Suspense>
      <template #default>
        <ParentComponent>
          <Suspense>
            <ChildComponent />
          </Suspense>
        </ParentComponent>
      </template>
    </Suspense>
    

4. 项目实战经验

"在我们的管理后台项目中:

  1. 路由级组件使用 Suspense 处理代码分割
  2. 数据密集型页面显示骨架屏
  3. 复杂表单异步验证状态管理
  4. 图片懒加载与错误回退"

5. 高级用法示例

  1. 自定义延迟策略

    const AsyncComponent = defineAsyncComponent({
      loader: () => import('./HeavyComponent.vue'),
      delay: 500, // 快速加载时不显示加载状态
      timeout: 10000 // 长时间加载显示超时
    });
    
  2. 与 Teleport 结合

    <Suspense>
      <Teleport to="#modals">
        <AsyncModal />
      </Teleport>
    </Suspense>
    
  3. 服务端渲染 (SSR)

    // 服务端同步渲染
    onServerPrefetch(async () => {
      await fetchData();
    });
    
  4. 组合式函数集成

    // useAsyncData.js
    export function useAsyncData() {
      const data = ref(null);
      const pending = ref(true);
      const error = ref(null);
      
      fetchData()
        .then(res => data.value = res)
        .catch(err => error.value = err)
        .finally(() => pending.value = false);
      
      return { data, pending, error };
    }
    

6. 常见问题解决方案

  1. 闪烁问题

    // 增加适当延迟
    delay: 200 // 快速加载时不显示加载状态
    
  2. 错误边界

    // 全局错误处理
    app.config.errorHandler = (err) => {
      // 统一处理错误
    };
    
  3. TypeScript 支持

    const AsyncComponent = defineAsyncComponent<{
      data: SomeType;
    }>(() => import('./MyComponent.vue'));
    
  4. 性能优化

    // 预加载组件
    const preload = () => import('./MyComponent.vue');
    

7. 性能优化技巧

  1. 代码分割

    // 路由级分割
    const routes = [
      {
        path: '/dashboard',
        component: defineAsyncComponent(() => import('./Dashboard.vue'))
      }
    ];
    
  2. 骨架屏优化

    <template #fallback>
      <SkeletonLoader />
    </template>
    
  3. 资源预加载

    // 提前加载关键资源
    onMounted(() => {
      import('./CriticalComponent.vue');
    });
    
  4. 缓存策略

    // 缓存已加载组件
    const cachedComponents = new Map();
    
    function loadComponent(name) {
      if (!cachedComponents.has(name)) {
        cachedComponents.set(name, defineAsyncComponent(() => 
          import(`./${name}.vue`)
        ));
      }
      return cachedComponents.get(name);
    }
    

8. 设计模式应用

  1. 策略模式

    const loadingStrategies = {
      default: DefaultSpinner,
      skeleton: SkeletonLoader,
      progress: ProgressBar
    };
    
    <template #fallback>
      <component :is="loadingStrategies[type]" />
    </template>
    
  2. 状态机模式

    const states = {
      loading: LoadingComponent,
      error: ErrorComponent,
      success: AsyncComponent
    };
    
    <component :is="states[currentState]" />
    
  3. 观察者模式

    const observers = [];
    
    function registerObserver(fn) {
      observers.push(fn);
    }
    
    function notifyAll() {
      observers.forEach(fn => fn());
    }
    

9. 单元测试策略

  1. 测试加载状态

    test('displays loading state', async () => {
      const wrapper = mount(Component);
      expect(wrapper.find('.loading').exists()).toBe(true);
      await flushPromises();
      expect(wrapper.find('.content').exists()).toBe(true);
    });
    
  2. 测试错误状态

    test('handles errors', async () => {
      mockFetch.mockRejectedValue(new Error('Failed'));
      const wrapper = mount(Component);
      await flushPromises();
      expect(wrapper.find('.error').exists()).toBe(true);
    });
    
  3. 测试异步数据

    test('renders async data', async () => {
      mockFetch.mockResolvedValue({ data: 'test' });
      const wrapper = mount(Component);
      await flushPromises();
      expect(wrapper.text()).toContain('test');
    });
    

10. 与 Vue 2 的对比

特性 Vue 2 解决方案 Vue 3 Suspense
异步组件 基本支持 深度集成
加载状态 手动管理 内置支持
错误处理 单独处理 统一错误边界
嵌套异步 难以管理 自动处理
组合式 API 不兼容 完美集成
SSR 支持 有限支持 完整支持
TypeScript 支持有限 完整类型支持
性能优化 需要手动优化 内置优化策略

11. 实际应用场景

  1. 路由懒加载

    const router = createRouter({
      routes: [
        {
          path: '/dashboard',
          component: defineAsyncComponent(() => 
            import('./views/Dashboard.vue')
          )
        }
      ]
    });
    
  2. 数据预取

    // 组件内部
    async setup() {
      const data = await fetchData();
      return { data };
    }
    
  3. 图片懒加载

    <Suspense>
      <template #default>
        <LazyImage src="image.jpg" />
      </template>
      <template #fallback>
        <Placeholder />
      </template>
    </Suspense>
    
  4. 权限验证

    const AuthComponent = defineAsyncComponent({
      loader: async () => {
        await checkPermissions();
        return import('./AdminPanel.vue');
      }
    });
    

12. 未来发展趋势

  1. Server Components

    // 服务端组件
    const ServerComponent = defineAsyncComponent({
      serverLoader: async () => {
        const data = await fetchServerData();
        return { data };
      }
    });
    
  2. Partial Hydration

    // 选择性水合
    const LazyHydrate = defineAsyncComponent({
      loader: () => import('./HeavyComponent.vue'),
      clientOnly: true
    });
    
  3. 智能预加载

    // 基于用户行为的预加载
    onHover(() => preloadComponent());
    
  4. 性能指标集成

    // 根据网络状况调整策略
    const strategy = navigator.connection.effectiveType === '4g' 
      ? 'eager' : 'lazy';
    

13. Vue 3 的 definePropsdefineEmits 深度解析

问题: 全面解析 <script setup> 中 props 和 emits 的声明方式、类型安全及最佳实践。

答案:

1. 核心概念

definePropsdefineEmits 是 Vue 3 的编译器宏,专为 <script setup> 设计:

  • defineProps: 声明组件接收的 props
  • defineEmits: 声明组件触发的事件
  • 无需导入,直接在 <script setup> 中使用
  • 提供完整的 TypeScript 类型支持

2. 基础用法

<script setup>
// 声明 props
const props = defineProps({
  title: {
    type: String,
    required: true,
    default: '默认标题'
  },
  count: Number
});

// 声明 emits
const emit = defineEmits(['update', 'delete']);

// 触发事件
function updateTitle() {
  emit('update', { newTitle: '新标题' });
}
</script>

<template>
  <h1>{{ title }}</h1>
  <button @click="updateTitle">更新标题</button>
</template>

3. 最佳实践指南

  1. Props 验证

    defineProps({
      // 基础类型检查
      id: Number,
      // 多类型
      author: [String, Number],
      // 必填
      title: {
        type: String,
        required: true
      },
      // 默认值
      likes: {
        type: Number,
        default: 0
      },
      // 对象默认值
      metadata: {
        type: Object,
        default: () => ({})
      },
      // 自定义验证
      content: {
        validator(value) {
          return ['success', 'warning', 'danger'].includes(value);
        }
      }
    });
    
  2. Emits 验证

    const emit = defineEmits({
      // 无验证
      click: null,
      // 带验证
      submit: (payload) => {
        if (payload.email && payload.password) {
          return true;
        }
        console.warn('Invalid submit payload!');
        return false;
      }
    });
    
  3. TypeScript 集成

    // 定义 Props 接口
    interface Props {
      title: string;
      count?: number;
    }
    
    // 定义 Emits 类型
    interface Emits {
      (e: 'update', value: string): void;
      (e: 'delete', id: number): void;
    }
    
    const props = defineProps<Props>();
    const emit = defineEmits<Emits>();
    
  4. 默认值与 TypeScript

    // 带默认值的类型定义
    withDefaults(defineProps<Props>(), {
      title: '默认标题',
      count: 0
    });
    

4. 项目实战经验

"在我们的管理后台项目中:

  1. 所有组件都使用 <script setup> 语法
  2. 复杂表单组件使用严格类型验证
  3. 事件命名遵循 kebab-case 规范
  4. 全局类型定义共享 props 接口"

5. 高级用法示例

  1. 动态 Props

    // 动态生成 props 类型
    type DynamicProps<T> = {
      [K in keyof T]: {
        type: PropType<T[K]>;
        required?: boolean;
      };
    };
    
    const props = defineProps(
      DynamicProps<{
        title: string;
        count: number;
      }>()
    );
    
  2. 泛型组件

    // 泛型组件示例
    interface Item {
      id: number;
      name: string;
    }
    
    const props = defineProps<{
      items: Item[];
      selected: Item;
    }>();
    
  3. 全局 Props 类型

    // types/props.ts
    export interface ButtonProps {
      type?: 'primary' | 'danger';
      size?: 'small' | 'medium';
    }
    
    // 组件中使用
    const props = defineProps<ButtonProps>();
    
  4. 组合式函数集成

    // useForm.ts
    export function useForm<T extends Record<string, any>>(props: T) {
      // 表单逻辑
    }
    
    // 组件中使用
    const props = defineProps<FormProps>();
    const { form } = useForm(props);
    

6. 常见问题解决方案

  1. Props 响应式丢失

    // 错误 ❌
    const { title } = defineProps(['title']);
    
    // 正确 ✅
    const props = defineProps(['title']);
    const title = toRef(props, 'title');
    
  2. 类型推断失败

    // 明确指定类型
    defineProps<{
      list: Array<{ id: number; name: string }>;
    }>();
    
  3. 默认值冲突

    // 使用 withDefaults 解决
    withDefaults(defineProps<{
      size?: 'small' | 'large';
    }>(), {
      size: 'small'
    });
    
  4. 事件命名规范

    // 使用 kebab-case
    defineEmits(['update-title', 'delete-item']);
    

7. 性能优化技巧

  1. Props 稳定性

    // 避免频繁变化的 props
    defineProps({
      config: {
        type: Object,
        default: () => ({})
      }
    });
    
  2. Memoized Props

    const props = defineProps(['items']);
    const sortedItems = computed(() => [...props.items].sort());
    
  3. 浅 Props

    defineProps<{
      largeData: ShallowRef<BigData>;
    }>();
    
  4. 事件节流

    const emit = defineEmits(['input']);
    
    const throttledEmit = _.throttle((value) => {
      emit('input', value);
    }, 300);
    

8. 设计模式应用

  1. 组件契约模式

    // Button.props.ts
    export interface ButtonProps {
      type?: ButtonType;
      size?: ButtonSize;
    }
    
    // Button.vue
    const props = defineProps<ButtonProps>();
    
  2. 事件总线模式

    // 定义严格类型的事件总线
    interface EventMap {
      'form-submit': { values: FormValues };
      'modal-close': void;
    }
    
    const emit = defineEmits<{
      [K in keyof EventMap]: (e: K, payload: EventMap[K]) => void;
    }>();
    
  3. 策略模式

    const props = defineProps<{
      validationStrategy: 'strict' | 'loose';
    }>();
    
    const strategies = {
      strict: (value) => value.length > 10,
      loose: (value) => value.length > 5
    };
    
    const isValid = computed(() => 
      strategies[props.validationStrategy](input.value)
    );
    

9. 单元测试策略

  1. 测试 Props

    test('renders with default props', () => {
      const wrapper = mount(Component);
      expect(wrapper.text()).toContain('默认标题');
    });
    
  2. 测试 Emits

    test('emits update event', async () => {
      const wrapper = mount(Component);
      await wrapper.find('button').trigger('click');
      expect(wrapper.emitted('update')).toBeTruthy();
    });
    
  3. 测试类型安全

    test('type checks', () => {
      mount(Component, {
        props: {
          title: 'Test', // 必须为 string
          count: 123     // 必须为 number
        }
      });
    });
    

10. 与 Options API 对比

特性 Options API <script setup>
Props 声明 props 选项 defineProps
Emits 声明 emits 选项 defineEmits
类型支持 有限支持 完整 TypeScript 支持
代码组织 分散 集中
默认值 选项内定义 withDefaults 辅助函数
响应式处理 自动 需要手动 toRef
代码量 较多 更简洁

11. 实际应用场景

  1. 表单组件

    defineProps<{
      modelValue: string;
      rules?: ValidationRule[];
    }>();
    
    defineEmits<{
      (e: 'update:modelValue', value: string): void;
      (e: 'validate', isValid: boolean): void;
    }>();
    
  2. 数据表格

    defineProps<{
      columns: TableColumn[];
      data: any[];
      loading?: boolean;
    }>();
    
    defineEmits<{
      (e: 'sort', column: TableColumn): void;
      (e: 'row-click', row: any): void;
    }>();
    
  3. 模态框组件

    defineProps<{
      show: boolean;
      title?: string;
      size?: 'sm' | 'md' | 'lg';
    }>();
    
    defineEmits<{
      (e: 'close'): void;
      (e: 'confirm', payload: any): void;
    }>();
    
  4. 分页组件

    defineProps<{
      current: number;
      pageSize: number;
      total: number;
    }>();
    
    defineEmits<{
      (e: 'change', page: number): void;
    }>();
    

12. 未来发展趋势

  1. 更智能的类型推断

    // 自动推断 emits 参数类型
    const emit = defineEmits<{
      update(value: string): void;
    }>();
    
  2. Props 解构响应式

    // 未来可能支持
    const { title } = defineProps(['title']); // 自动保持响应性
    
  3. 编译时验证

    // 编译时检查 props 和 emits
    @ValidateProps({...})
    @ValidateEmits([...])
    const props = defineProps();
    
  4. 组合式 Props

    // 组合多个 props 定义
    const props = defineProps(
      withDefaults(baseProps, { ... }),
      withValidation(validationRules)
    );
    

14. Vue 3 动态样式绑定深度解析

问题: 全面解析 Vue 3 中动态绑定样式的各种方式、性能优化和最佳实践。

答案:

1. 核心概念

Vue 3 提供了多种动态绑定样式的方式:

  • 对象语法:style="{ property: value }"
  • 数组语法:style="[baseStyles, overridingStyles]"
  • CSS 变量:通过 v-bind 绑定 CSS 变量
  • 自动前缀:Vue 会自动为需要浏览器前缀的 CSS 属性添加前缀

2. 基础用法

<template>
  <!-- 对象语法 -->
  <div :style="{ 
    color: activeColor, 
    fontSize: fontSize + 'px',
    'background-color': bgColor 
  }">
    对象语法
  </div>

  <!-- 数组语法 -->
  <div :style="[baseStyles, overridingStyles]">
    数组语法
  </div>

  <!-- CSS 变量 -->
  <div :style="{
    '--primary-color': themeColor,
    '--size': size + 'px'
  }">
    CSS 变量
  </div>
</template>

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

const activeColor = ref('red');
const fontSize = ref(16);
const bgColor = ref('#f0f0f0');
const themeColor = ref('#42b983');
const size = ref(100);

const baseStyles = {
  padding: '10px',
  margin: '5px'
};

const overridingStyles = {
  color: 'blue',
  fontWeight: 'bold'
};
</script>

3. 最佳实践指南

  1. 性能优化

    // 避免频繁变化的样式对象
    const styleObject = computed(() => ({
      transform: `scale(${scale.value})`
    }));
    
  2. CSS 变量最佳实践

    <template>
      <div class="styled-box" :style="cssVars">
        使用 CSS 变量
      </div>
    </template>
    
    <script setup>
    const cssVars = {
      '--primary-color': '#42b983',
      '--border-radius': '8px'
    };
    </script>
    
    <style>
    .styled-box {
      color: var(--primary-color);
      border-radius: var(--border-radius);
    }
    </style>
    
  3. 动态类名与样式结合

    <div 
      class="button"
      :class="{ active: isActive }"
      :style="buttonStyles"
    >
      按钮
    </div>
    
  4. 响应式样式

    const windowWidth = ref(window.innerWidth);
    
    onMounted(() => {
      window.addEventListener('resize', () => {
        windowWidth.value = window.innerWidth;
      });
    });
    
    const responsiveStyle = computed(() => ({
      width: windowWidth.value > 768 ? '50%' : '100%'
    }));
    

4. 项目实战经验

"在我们的管理后台项目中:

  1. 主题切换使用 CSS 变量实现
  2. 动画效果使用 transform 优化性能
  3. 响应式布局结合样式绑定
  4. 动态图表样式通过计算属性生成"

5. 高级用法示例

  1. 动画性能优化

    const progress = ref(0);
    const progressStyle = computed(() => ({
      transform: `translateX(${progress.value}%)`,
      willChange: 'transform' // 提示浏览器优化
    }));
    
  2. 动态主题切换

    const themes = {
      light: {
        '--bg-color': '#ffffff',
        '--text-color': '#333333'
      },
      dark: {
        '--bg-color': '#1a1a1a',
        '--text-color': '#f0f0f0'
      }
    };
    
    const currentTheme = ref('light');
    const themeVars = computed(() => themes[currentTheme.value]);
    
  3. 与 Transition 结合

    <Transition name="fade">
      <div 
        v-show="show"
        :style="{
          position: 'absolute',
          transition: 'all 0.3s ease'
        }"
      >
        内容
      </div>
    </Transition>
    
  4. TypeScript 支持

    interface StyleObject {
      color?: string;
      backgroundColor?: string;
      [key: string]: string | number | undefined;
    }
    
    const styles: StyleObject = {
      color: 'red',
      fontSize: 16
    };
    

6. 常见问题解决方案

  1. 样式覆盖问题

    <!-- 使用 !important 时 -->
    <div :style="{ color: 'red !important' }"></div>
    
    <!-- 更好的解决方案 -->
    <div class="important-color" :style="{ color }"></div>
    
    <style>
    .important-color {
      color: red !important;
    }
    </style>
    
  2. 单位自动添加

    // Vue 会自动为需要单位的属性添加 px
    :style="{ width: 100 }" // 渲染为 width: 100px
    
    // 禁用自动添加
    :style="{ width: '100' }" // 渲染为 width: 100
    
  3. 性能瓶颈

    // 避免在模板中直接计算
    :style="{ width: count * 10 + 'px' }" // ❌
    
    // 改用计算属性
    const width = computed(() => count.value * 10 + 'px'); // ✅
    
  4. 浏览器前缀处理

    // Vue 会自动添加需要的浏览器前缀
    :style="{ transform: 'scale(1.1)' }"
    // 渲染为 -webkit-transform: scale(1.1); transform: scale(1.1);
    

7. 性能优化技巧

  1. 样式对象复用

    // 共享基础样式
    const baseStyles = {
      padding: '10px',
      margin: '5px'
    };
    
    const buttonStyles = computed(() => ({
      ...baseStyles,
      backgroundColor: isActive.value ? 'blue' : 'gray'
    }));
    
  2. CSS 变量优化

    // 在根元素设置变量
    const rootStyles = {
      '--primary-color': '#42b983',
      '--secondary-color': '#35495e'
    };
    
    // 组件内直接使用变量
    .button {
      background-color: var(--primary-color);
    }
    
  3. 避免内联关键样式

    <!-- 避免 -->
    <div :style="{ display: 'flex' }">
    
    <!-- 推荐 -->
    <div class="flex-container">
    
    <style>
    .flex-container {
      display: flex;
    }
    </style>
    
  4. 硬件加速

    const animatedStyle = {
      transform: 'translateZ(0)', // 触发硬件加速
      willChange: 'transform'
    };
    

8. 设计模式应用

  1. 策略模式

    const sizeStrategies = {
      small: { padding: '4px', fontSize: '12px' },
      medium: { padding: '8px', fontSize: '14px' },
      large: { padding: '12px', fontSize: '16px' }
    };
    
    const size = ref('medium');
    const sizeStyle = computed(() => sizeStrategies[size.value]);
    
  2. 状态模式

    const stateStyles = {
      normal: { opacity: 1 },
      disabled: { opacity: 0.5 },
      loading: { opacity: 0.8 }
    };
    
    const state = ref('normal');
    const currentStyle = computed(() => stateStyles[state.value]);
    
  3. 工厂模式

    function createButtonStyle(variant) {
      const base = { padding: '8px 16px' };
      switch(variant) {
        case 'primary': return { ...base, backgroundColor: 'blue' };
        case 'danger': return { ...base, backgroundColor: 'red' };
        default: return base;
      }
    }
    
    const buttonStyle = createButtonStyle('primary');
    

9. 单元测试策略

  1. 测试样式绑定

    test('applies correct styles', () => {
      const wrapper = mount(Component, {
        props: { color: 'red' }
      });
      expect(wrapper.attributes('style')).toContain('color: red');
    });
    
  2. 测试响应式样式

    test('responds to window resize', async () => {
      window.innerWidth = 500;
      window.dispatchEvent(new Event('resize'));
      await nextTick();
      expect(wrapper.attributes('style')).toContain('width: 100%');
    });
    
  3. 测试 CSS 变量

    test('sets CSS variables', () => {
      const wrapper = mount(Component);
      expect(wrapper.attributes('style')).toContain('--primary-color: #42b983');
    });
    

10. 与 Vue 2 的对比

特性 Vue 2 Vue 3
自动前缀 需要手动配置 自动添加
性能优化 较少优化 更好的样式更新性能
CSS 变量 支持有限 完整支持
数组语法 支持 支持,性能更好
TypeScript 类型支持有限 完整类型支持
单位处理 需要手动添加 自动处理

11. 实际应用场景

  1. 主题切换

    const themes = {
      light: { '--bg': '#fff', '--text': '#333' },
      dark: { '--bg': '#222', '--text': '#fff' }
    };
    
    const currentTheme = ref('light');
    
  2. 动态图表

    const chartStyles = computed(() => ({
      '--chart-height': `${chartHeight.value}px`,
      '--chart-width': `${chartWidth.value}px`
    }));
    
  3. 响应式布局

    const responsiveStyles = computed(() => ({
      gridTemplateColumns: `repeat(${columns.value}, 1fr)`
    }));
    
  4. 动画控制

    const animationStyles = computed(() => ({
      transform: `translateX(${offset.value}px)`,
      transition: `transform ${duration.value}s ease`
    }));
    

12. 未来发展趋势

  1. CSS-in-JS 集成

    const styles = useStyles({
      button: {
        padding: '8px 16px',
        backgroundColor: props => props.color
      }
    });
    
  2. 原子化 CSS

    const classNames = computed(() => [
      'p-4',
      'm-2',
      isActive ? 'bg-blue-500' : 'bg-gray-500'
    ]);
    
  3. 设计系统集成

    const styles = useDesignSystem({
      variant: 'primary',
      size: 'medium'
    });
    
  4. 性能监控

    // 跟踪样式更新性能
    const stopTrace = startTrace('style-updates');
    // ...样式更新
    stopTrace();
    

15. Vue 3 中如何注册全局组件?

问题: 注册一个可全局使用的按钮组件。

答案:

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import MyButton from './components/MyButton.vue';

const app = createApp(App);
app.component('MyButton', MyButton);
app.mount('#app');

16. Vue 3 中如何实现自定义指令?

问题: 实现一个高亮指令 v-highlight

答案:

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.directive('highlight', {
  mounted(el) {
    el.style.backgroundColor = '#f0e68c';
  }
});

app.mount('#app');

17. Vue 3 中的 nextTick() 怎么用?

问题: 修改 DOM 后等待更新完成。

答案:

import { nextTick } from 'vue';

async function updateData() {
  this.message = '更新后的内容';
  await nextTick();
  console.log('DOM 已更新');
}

18. Vue 3 中如何实现组件懒加载?

问题: 使用异步组件实现路由懒加载。

答案:

import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/LazyComponent.vue')
);

export default {
  components: {
    AsyncComponent
  }
};

19. Vue 3 中的 emitter 是什么?如何使用?

问题: 实现非父子组件之间的通信。

答案:

npm install mitt
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();

// 发送事件
import { emitter } from './eventBus';
emitter.emit('update', 'Hello');

// 接收事件
import { emitter } from './eventBus';
emitter.on('update', (msg) => {
  console.log(msg);
});

20. Vue 3 中如何使用 v-model 实现双向绑定?

问题: 实现一个输入框组件的双向绑定。

答案:

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

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
};
</script>

高频考点补充

考点 描述
Composition API 替代 Options API,提升逻辑复用
Reactivity API ref, reactive, toRefs, watch, computed
Teleport / Portal 渲染到任意 DOM 节点
Fragment 支持多个根节点
TypeScript 支持 完整 TS 类型推导
Custom Renderer 可定制渲染器(如 Canvas、SSR)
性能优化 更小的体积、更快的 Diff 算法
组合函数(Composable) 封装可复用逻辑
Vue 3 的编译器优化 Block Tree、Patch Flags
Vue 3 的生态支持 Vite、Pinia、Vue Router 4、Element Plus

以上来自Juleon博客,转载请注明出处。