xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • JavaScript 100 题

    • JavaScript 面试 100 题及参考答案
  • HTML+CSS 50题

    • 前端面试之 HTML+CSS 50题及参考答案
  • Vue 50题

    • 前端面试之 Vue 50题及参考答案
  • React 50题

    • 前端面试之 React 50题及参考答案
  • 前端工程化50题

    • 前端面试之工程化50题及参考答案
  • 实践案例50题

    • 前端面试之实践案例50题及参考答案

前端面试之 Vue 50题及参考答案

Vue3 作为现代前端框架的佼佼者,其新特性和性能优化是面试中的考察重点。本文将深入剖析 50 道高频 Vue3 面试题,助你全面掌握核心概念。

一、基础概念与新特性

1. Vue3 相比 Vue2 有哪些主要改进?

Vue3 带来了多项重大革新:

  • 响应式系统:使用 Proxy 替代 Object.defineProperty,能监听对象属性的添加、删除及数组索引变化,解决了 Vue2 的响应式局限性。
  • 性能提升:包括重构的虚拟 DOM (引入 PatchFlag、静态提升)、事件缓存等,渲染速度更快。
  • Tree-shaking:模块化架构支持,未使用的代码不会被打包,减小应用体积。
  • Composition API:提供更灵活的逻辑组织和复用方式,替代 Options API。
  • TypeScript 支持:源码使用 TypeScript 重写,提供更好的类型推断。
  • 新组件:引入 Teleport、Suspense、Fragment 等内置组件。
  • 自定义渲染器:支持更灵活的自定义渲染器 API。

2. 什么是 Composition API?它的作用是什么?

Composition API 是 Vue3 引入的一套新的 API,允许开发者使用导入的函数来组织组件逻辑,而不是依赖选项(如 data, methods, computed)。它的核心是 setup 函数。

作用:

  • 更好的逻辑组织:将相关联的逻辑代码组织在一起,而不是按选项分散。
  • 逻辑复用:通过自定义组合式函数,可以轻松地在不同组件间复用逻辑,解决了 Vue2 Mixins 的命名冲突和来源不清晰问题。
  • 更好的 TypeScript 支持:与 TypeScript 的类型推断系统配合得更好。

示例:一个简单的计数器逻辑复用

// useCounter.js - 一个组合式函数
import { ref } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count,
    increment,
    decrement
  };
}

// MyComponent.vue - 在组件中使用
import { useCounter } from './useCounter';

export default {
  setup() {
    const { count, increment, decrement } = useCounter();
    return {
      count,
      increment,
      decrement
    };
  }
};

3. Vue3 的生命周期钩子有哪些变化?

Vue3 的生命周期钩子与 Vue2 类似,但有一些重要变化和新增钩子:

Vue2 选项Vue3 组合式 API Hook说明
beforeCreate-被 setup() 替代
created-被 setup() 替代
beforeMountonBeforeMount组件挂载前调用
mountedonMounted组件挂载后调用
beforeUpdateonBeforeUpdate组件更新前调用
updatedonUpdated组件更新后调用
beforeDestroyonBeforeUnmount名称变更,组件卸载前调用
destroyedonUnmounted名称变更,组件卸载后调用
activatedonActivated被 <keep-alive> 缓存的组件激活时调用
deactivatedonDeactivated被 <keep-alive> 缓存的组件失活时调用
errorCapturedonErrorCaptured捕获来自子孙组件的错误时调用
-onRenderTracked新增 (开发模式),当响应式依赖被追踪时调用 (调试用)
-onRenderTriggered新增 (开发模式),当响应式依赖触发组件重新渲染时调用 (调试用)

主要变化:

  • beforeDestroy 和 destroyed 被重命名为 onBeforeUnmount 和 onUnmounted,更准确地描述了其功能。
  • setup() 函数在 beforeCreate 和 created 生命周期之间运行,意味着在这两个钩子中编写的代码都应直接在 setup() 中编写。
  • 所有生命周期钩子都需要从 vue 中导入并在 setup() 中使用。
  • 可以在 setup() 中多次注册同一个生命周期钩子,它们会按照注册顺序依次执行。

示例:在 setup 中使用生命周期钩子

import { onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      console.log('Component is mounted!');
      // 执行一些初始化操作,例如添加事件监听器
    });

    onUnmounted(() => {
      console.log('Component is unmounted!');
      // 执行清理操作,例如移除事件监听器
    });
  }
};

4. 什么是 setup 函数?

setup 函数是 Composition API 的入口点,在组件创建之前执行(在 beforeCreate 和 created 生命周期钩子之前),此时组件实例尚未生成,因此 this 不可用。

参数:

  • props: 组件接收的 props,是响应式的。
  • context: 一个普通 JavaScript 对象,暴露了在组件中可能常用的三个属性:
    • attrs: 包含未在 props 中声明的 attribute。
    • slots: 包含所有传入的插槽内容。
    • emit: 用于触发事件的方法。

返回值: setup 返回一个对象,该对象的所有属性都会暴露给组件的模板和其他的选项(如 methods)使用。

作用:

  • 定义响应式数据、计算属性、方法等。
  • 注册生命周期钩子。
  • 返回模板需要使用的数据和方法。

示例:基本的 setup 用法

import { ref } from 'vue';

export default {
  props: {
    title: String
  },
  setup(props, context) {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };

    // 使用 context
    context.emit('customEvent');

    // 返回模板可用的内容
    return {
      count,
      increment
    };
  }
};

5. Vue3 中的响应式系统是如何工作的?

Vue3 使用 JavaScript 的 Proxy 对象来实现响应式,这相较于 Vue2 的 Object.defineProperty 是一次重大的升级。

工作原理简述:

  1. ** reactive 函数**: 当你使用 reactive() 包裹一个对象时,Vue 会返回该对象的 Proxy 代理。这个 Proxy 可以拦截(intercept)对目标对象的各种操作,例如属性读取 (get)、属性设置 (set)、属性删除 (deleteProperty) 等。
  2. 依赖追踪 (Track): 当在副作用函数(例如组件的渲染函数或 watchEffect)中读取 (get) 一个响应式对象的属性时,Vue 会追踪这个属性与当前正在运行的副作用函数的依赖关系,并将其存储起来。
  3. 触发更新 (Trigger): 当你修改 (set) 或删除 (deleteProperty) 一个响应式对象的属性时,Proxy 会拦截这个操作。Vue 会找到所有依赖于这个属性的副作用函数,并重新执行它们,从而触发组件的重新渲染或 watchEffect 的重新执行。

Proxy 的优势:

  • 可检测新增/删除属性: 无需使用 Vue.set/Vue.delete。
  • 可检测数组索引变化及 .length 修改。
  • 性能更好: 按需拦截,惰性创建子对象的 Proxy(只有在访问深层属性时才会递归创建)。
  • 支持更多数据结构: 如 Map, Set, WeakMap, WeakSet。

简易代码模拟:

// 极简版的 reactive 实现原理
function reactive(target) {
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key); // 依赖追踪:记录“谁读取了这个对象的这个属性”
      return Reflect.get(obj, key);
    },
    set(obj, key, value) {
      const result = Reflect.set(obj, key, value);
      trigger(obj, key); // 触发更新:通知所有依赖于此属性的地方“值变了,快更新!”
      return result;
    },
    deleteProperty(obj, key) {
      const result = Reflect.deleteProperty(obj, key);
      trigger(obj, key); // 触发更新
      return result;
    }
  });
}

// 存储依赖关系的桶(实际Vue的实现更复杂)
const targetMap = new WeakMap(); // 用于存储每个目标对象及其键的依赖

function track(target, key) {
  // ... 收集当前正在运行的副作用函数到 targetMap[target][key] 中
}

function trigger(target, key) {
  // ... 从 targetMap[target][key] 中取出所有副作用函数并执行
}

二、Composition API 核心概念

6. ref 和 reactive 的区别是什么?它们的使用场景是怎样的?

ref 和 reactive 是 Vue3 创建响应式数据的两个核心函数,它们有不同的用途。

特性refreactive
数据类型主要用于基本类型(字符串、数字、布尔值),也可用于对象和数组主要用于对象或数组等引用类型
返回值返回一个响应式对象(RefImpl),其值在 .value 属性中返回原始对象的 Proxy 代理对象
访问方式在 JavaScript 中需要通过 .value 访问;在模板中自动解包,无需 .value直接访问属性即可
解构/扩展使用 toRef 或 toRefs 解构才能保持响应性直接解构或扩展会丢失响应性,需使用 toRefs
模板引用可用于模板 ref,获取 DOM 元素或组件实例不能用于模板 ref

使用场景:

  • ref: 当你有一个独立的基本类型值需要变为响应式时(如 count, inputValue),或者在逻辑上它是一个需要被“引用”的单一值。也常用于模板 ref。
    const count = ref(0);
    const inputEl = ref(null); // 模板ref
  • reactive: 当你有一组** logically related**(逻辑上相关联)的数据,并且它们通常以对象的形式组织在一起时(如表单数据、模块状态)。
    const formState = reactive({
      username: '',
      password: '',
      rememberMe: false
    });

7. computed 和 watch 的区别是什么?

computed 和 watch 都用于响应数据变化,但目的和用法不同。

特性computedwatch / watchEffect
目的声明一个依赖其他响应式数据的计算属性,其值基于依赖缓存执行副作用操作,如监听数据变化后发起异步请求、操作DOM等
返回值返回一个 ref 对象,代表计算后的值无返回值
缓存有缓存,只有依赖变化时才重新计算无缓存,只要监听的值变化就执行
异步不能包含异步操作可以包含异步操作
立即执行默认不会立即执行,依赖变化才执行watch 可配置 immediate: true 来立即执行;watchEffect 会立即执行

computed 示例:

import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// 计算属性 fullName
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});
// 在模板中使用:{{ fullName }} (无需 .value)

watch 示例:

import { ref, watch } from 'vue';

const userId = ref(1);
const userData = ref(null);

// 监听 userId 的变化,并异步获取用户数据
watch(userId, async (newId, oldId) => {
  const response = await fetch(`/api/users/${newId}`);
  userData.value = await response.json();
}, { immediate: true }); // 立即执行一次,在组件创建时获取初始用户数据

watchEffect 示例:

import { ref, watchEffect } from 'vue';

const count = ref(0);

// watchEffect 会自动追踪其内部使用的所有响应式依赖
watchEffect(() => {
  console.log(`Count is now: ${count.value}`); // 会自动追踪 count.value
  // 当 count.value 变化时,这个函数会重新执行
});

8. 什么是 watchEffect?它与 watch 有何不同?

watchEffect 是 Vue3 引入的一个新的 API,用于立即运行一个副作用函数,并自动追踪该函数内部所使用的所有响应式依赖,并在这些依赖发生更改时重新运行该函数。

与 watch 的主要区别:

特性watchwatchEffect
依赖源指定方式显式指定一个或多个要监听的响应式引用或 getter 函数自动收集副作用函数中使用的所有响应式属性
旧值在回调函数中可以获取旧值和新值无法获取旧值
立即执行默认不立即执行,可配置 immediate: true 来实现立即执行,然后在依赖变更时重新执行
适用场景适用于需要明确知道是哪个依赖发生了变化,或者需要对比旧值和新值的场景适用于依赖关系不那么明确,或者多个依赖中任何一个变化都需要执行副作用的场景(如日志记录、请求取消)

示例对比:

// 使用 watch:需要明确指定要监听的源
const firstName = ref('');
const lastName = ref('');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
});

// 使用 watchEffect:自动追踪 firstName 和 lastName
watchEffect(() => {
  console.log(`Full name is: ${firstName.value} ${lastName.value}`);
  // 这个函数会因 firstName.value 或 lastName.value 的变化而重新执行
});

9. 如何在 setup 中访问组件实例?

在 setup 中,由于执行时机过早,this 不再是组件实例的引用。要访问组件实例,可以使用 getCurrentInstance 函数。

import { getCurrentInstance } from 'vue';

export default {
  setup() {
    const instance = getCurrentInstance(); // 获取当前组件实例

    console.log(instance); // 包含 proxy, ctx, parent 等属性
    // 通过 instance.proxy 可以访问组件上下文,类似于 this
    // 例如:instance.proxy.$route, instance.proxy.$router (如果使用了Vue Router)

    // 注意:getCurrentInstance 主要用于高级用例或库的开发,在绝大多数应用代码中应避免使用,
    // 因为它会使代码更难以理解和测试,且可能不兼容 SSR。
  }
};

注意: getCurrentInstance 仅在开发或调试中有特定用途,在业务逻辑中应尽量避免使用,以保持组合函数的纯粹性和可复用性。通常,需要的功能(如访问 root 或 parent)可以通过 provide/inject 或 props 来实现。

10. 什么是 toRef 和 toRefs?它们有什么用?

toRef 和 toRefs 是用于保持响应式对象属性响应性的工具函数,在处理组合式函数返回或解构 reactive 对象时非常有用。

  • toRef: 为响应式对象 (reactive 创建) 上的某个属性创建一个 ref。这个 ref 是响应式地链接到原对象的该属性上,修改 ref 的值会更新原对象,反之亦然。

    import { reactive, toRef } from 'vue';
    const state = reactive({ foo: 1, bar: 2 });
    const fooRef = toRef(state, 'foo'); // 创建一个链接到 state.foo 的 ref
    fooRef.value++; // state.foo 现在是 2
    console.log(state.foo); // 2
    state.foo++; // fooRef.value 现在是 3
    console.log(fooRef.value); // 3
  • toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向原对象相应属性的 ref。这在从组合式函数返回响应式对象或在解构时保持响应性非常有用。

    import { reactive, toRefs } from 'vue';
    
    // 在组合式函数中
    function useFeatureX() {
      const state = reactive({ x: 0, y: 0 });
      // ... 逻辑操作 state
      return toRefs(state); // 返回时转换为 refs,这样使用方可以解构而不会失去响应性
    }
    
    // 在组件中使用
    export default {
      setup() {
        // 可以解构,同时保持响应性!
        const { x, y } = useFeatureX();
        return { x, y };
      }
    };

toRefs 在解构 reactive 对象时的应用:

import { reactive, toRefs } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: 'Vue'
    });

    // 直接解构会失去响应性!
    // let { count, name } = state; // ❌ 非响应式

    // 使用 toRefs 解构保持响应性
    let { count, name } = toRefs(state); // ✅ 现在是两个 ref

    // 在 JavaScript 中访问需要 .value
    console.log(count.value);
    // 在模板中可以直接使用,会自动解包
    return {
      count,
      name
    };
  }
};

三、组件通信

11. Vue3 中组件通信的方式有哪些?

Vue3 提供了多种组件通信方式,适用于不同场景:

  1. Props / Emits:最基础的父子组件通信方式。
  2. v-model / .sync:语法糖,实现父子组件数据的双向绑定。
  3. Refs:父组件通过 ref 获取子组件实例,调用其方法或访问其数据。
  4. Provide / Inject:跨层级组件通信,祖先组件提供数据,后代组件注入使用。
  5. Event Bus:利用第三方库(如 mitt)实现全局事件监听和触发,适用于任意组件间通信。
  6. Vuex / Pinia:全局状态管理库,集中管理所有组件共享的状态。
  7. 插槽 (Slots):父组件向子组件传递模板内容。
    • 默认插槽
    • 具名插槽
    • 作用域插槽:子组件向父组件的插槽内容传递数据。

12. provide 和 inject 的作用是什么?如何使用?

provide 和 inject 用于实现跨层级组件通信,通常用于深层嵌套的组件之间传递数据,避免繁琐的 “prop 逐级透传” (Prop Drilling)。

  • provide (提供):在祖先组件中使用,提供一个值,可以被所有后代组件注入。
  • inject (注入):在任何后代组件中使用,注入一个由祖先组件提供的值。

使用方法:

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

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

    // 提供(provide)一个值(可以是响应式的)
    provide('theme', theme); // 提供响应式数据
    provide('toggleTheme', toggleTheme); // 提供方法

    return { theme, toggleTheme };
  }
};

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

export default {
  setup() {
    // 注入(inject)祖先组件提供的值
    const theme = inject('theme', 'light'); // 第二个参数是默认值(可选)
    const toggleTheme = inject('toggleTheme');

    return {
      theme,
      toggleTheme
    };
  }
};

注意: 为了确保注入方和提供方之间的响应性链接,提供方应尽可能提供响应式数据(如 ref 或 reactive)。

13. 如何在 Vue3 中使用事件总线 (Event Bus)?

Vue3 不再内置事件总线(因为 $on, $off 等实例方法已被移除),但可以使用第三方库如 mitt 或 tiny-emitter 轻松实现。

使用 mitt 的步骤:

  1. 安装: npm install mitt
  2. 创建事件总线实例:
    // eventBus.js
    import mitt from 'mitt';
    const emitter = mitt();
    export default emitter;
  3. 在组件中发射 (emit) 事件:
    // ComponentA.vue (发送事件)
    import emitter from './eventBus';
    
    export default {
      setup() {
        const sendMessage = () => {
          emitter.emit('user-login', { username: 'alice' });
        };
        return { sendMessage };
      }
    };
  4. 在组件中监听 (on) 事件:
    // ComponentB.vue (接收事件)
    import emitter from './eventBus';
    import { onUnmounted } from 'vue';
    
    export default {
      setup() {
        const handleLogin = (user) => {
          console.log('User logged in:', user);
        };
    
        // 监听事件
        emitter.on('user-login', handleLogin);
    
        // 重要:在组件卸载时移除监听器,防止内存泄漏
        onUnmounted(() => {
          emitter.off('user-login', handleLogin);
        });
      }
    };

14. 什么是作用域插槽 (Scoped Slots)?

作用域插槽允许子组件将数据传递给它插槽中的内容,父组件可以决定如何渲染这些数据。这相当于子组件提供数据,父组件决定数据的呈现样式。

使用方法:

  1. 子组件:在 <slot> 元素上绑定要传递的属性(称为“插槽 props”)。
  2. 父组件:使用 v-slot 指令(或其简写 #)来接收插槽 props。

示例:

<!-- 子组件 MyList.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <!-- 将 item 数据传递给插槽 -->
      <slot name="item" :item="item" :index="index">
        <!-- 默认内容,如果父组件没有提供则显示 -->
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: ['items']
}
</script>
<!-- 父组件 -->
<template>
  <MyList :items="userList">
    <!-- 使用 v-slot:name="slotProps" 接收数据 -->
    <template #item="{ item, index }">
      <!-- 父组件可以自定义如何渲染每一项 -->
      <span style="color: blue;">第{{ index + 1 }}位:{{ item.name }} - {{ item.age }}</span>
    </template>
  </MyList>
</template>

<script>
import MyList from './MyList.vue';

export default {
  components: { MyList },
  data() {
    return {
      userList: [
        { id: 1, name: 'Alice', age: 25 },
        { id: 2, name: 'Bob', age: 30 }
      ]
    };
  }
};
</script>

15. 如何使用 expose 和 ref 实现组件通信?

在 Vue3 中,子组件可以使用 defineExpose(在 <script setup> 中)或 expose 选项(在选项式 API 中)来显式暴露其内部的属性或方法。父组件则通过 ref 来获取子组件实例并调用这些暴露出来的方法。

步骤:

  1. 子组件:使用 defineExpose 暴露公共接口。
  2. 父组件:声明一个 ref 并将其绑定到子组件上,然后通过 ref.value 来访问子组件暴露的方法。

示例:

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <p>Count: {{ count }}</p>
  </div>
</template>

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

const count = ref(0);

const increment = () => {
  count.value++;
};

// 暴露 count 和 increment 方法给父组件
defineExpose({
  count,
  increment
});
</script>
<!-- 父组件 ParentComponent.vue -->
<template>
  <ChildComponent ref="childRef" />
  <button @click="callChildMethod">Call Child's Method</button>
</template>

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

// 声明一个 ref 来引用子组件实例
const childRef = ref(null);

const callChildMethod = () => {
  // 通过 ref 访问子组件暴露的方法
  if (childRef.value) {
    childRef.value.increment();
    console.log('Child count is:', childRef.value.count);
  }
};
</script>

这种方式使得父子组件间的通信更加明确和可控。

四、响应式系统进阶

16. 什么是响应式失效?如何解决?

响应式失效是指你修改了数据,但 Vue 无法检测到这个变化,因此视图没有相应更新。常见原因和解决方法如下:

常见原因及解决方案:

  1. 直接给响应式对象添加新属性:

    const obj = reactive({ a: 1 });
    obj.b = 2; // ❌ 添加新属性 b,不是响应式的

    解决:

    • 提前在 reactive 中声明所有属性。
    • 使用 Object.assign 或扩展运算符创建新对象:
      obj = reactive({ ...obj, b: 2 }); // ✅
    • 对 reactive 对象,可以使用 obj.b = value,因为 Proxy 支持。但对于 ref 持有的对象,则需注意。
  2. 直接通过索引设置数组项或修改数组长度:

    const arr = reactive([1, 2, 3]);
    arr[3] = 4; // ❌ 不是响应式的
    arr.length = 2; // ❌ 不是响应式的

    解决:

    • 使用数组方法:push, pop, shift, unshift, splice, sort, reverse。
    • 使用 arr.splice(index, 1, newValue) 替换元素。
    • 重新赋值:arr.value = [...arr.value, newItem] (对于 ref 数组)。
  3. 解构丢失响应性:

    const state = reactive({ x: 1, y: 2 });
    const { x, y } = state; // ❌ x, y 不再是响应式的

    解决: 使用 toRefs:

    const { x, y } = toRefs(state); // ✅ x, y 是响应式的 ref

17. Vue3 中的响应式代理有哪些局限性?

尽管 Proxy 很强大,但仍有一些边缘情况需要注意:

  • 原始值: Proxy 只能代理对象,不能代理原始值(如字符串、数字)。这就是为什么需要 ref 来包装原始值。
  • 相等性比较: 代理后的对象与原始对象不全等(===),但通过 Proxy 访问属性与访问原始对象属性效果基本一致。
  • 浏览器内置对象: 代理像 Map, Set, Date, Promise 这样的内置对象可能需要额外处理,因为它们具有特殊的内部槽(internal slots),但 Vue3 内部已对这些做了处理。
  • 性能开销: 对于非常大的、深度嵌套的对象,响应式转换会带来一定的性能开销。在极少数情况下,如果性能成为问题,可以考虑使用 shallowRef 或 shallowReactive。

18. 什么是 shallowReactive 和 shallowRef?

shallowReactive 和 shallowRef 是 Vue3 提供的用于创建浅层响应式数据的 API,主要用于性能优化。

  • shallowReactive: 创建一个响应式代理,但只跟踪对象第一层属性的响应性。深层嵌套的属性不会被自动转换为响应式。

    import { shallowReactive } from 'vue';
    const state = shallowReactive({
      count: 0,
      user: { name: 'Alice' } // ❌ user 内部的属性不是响应式的
    });
    state.count++; // ✅ 触发更新
    state.user.name = 'Bob'; // ❌ 不会触发更新
  • shallowRef: 创建一个 ref,但只跟踪其 .value 本身的更改,不关心 .value 内部的值(如果 .value 是一个对象)。

    import { shallowRef } from 'vue';
    const data = shallowRef({ count: 0 });
    data.value = { count: 1 }; // ✅ 触发更新 (.value 被重新赋值)
    data.value.count++; // ❌ 不会触发更新 (内部属性变化)

使用场景: 当你有一个很大的对象或列表,但只有顶层属性的变化需要触发更新,或者你确信深层属性不会变化时,可以使用它们来避免不必要的深度响应式转换开销。

19. 什么是 readonly 和 shallowReadonly?

readonly 和 shallowReadonly 用于创建只读的响应式代理,防止数据被意外修改,常用于传递 props 或上下文数据。

  • readonly: 接受一个对象(响应式或普通)或 ref,返回一个原值的深层只读代理。任何尝试修改的操作都会被阻止。

    import { reactive, readonly } from 'vue';
    const original = reactive({ count: 0 });
    const copy = readonly(original);
    copy.count++; // ❌ 警告并阻止操作!
    original.count++; // ✅ 原对象依然可修改,copy.count 也会同步变为 1
  • shallowReadonly: 创建一个浅层只读代理。只有第一层属性是只读的,深层属性仍然可以被修改。

    import { shallowReadonly } from 'vue';
    const state = shallowReadonly({
      count: 0,
      user: { name: 'Alice' }
    });
    state.count++; // ❌ 警告并阻止操作!
    state.user.name = 'Bob'; // ✅ 可以修改深层属性(但不会触发视图更新,除非原对象是响应式的)

使用场景: 向子组件传递 props 时,Vue 内部会自动将其转换为只读的,但你也可以手动使用它们来明确表达数据不应被修改的意图。

20. 如何手动触发响应式更新?

在极少数情况下,你可能需要强制更新视图,即使依赖没有“正常”地变化。可以使用以下方法:

  • triggerRef(ref): 手动强制触发一个与 shallowRef 关联的副作用。如果你修改了 shallowRef 的深层属性,这很有用。

    import { shallowRef, triggerRef } from 'vue';
    const shallowObj = shallowRef({ count: 0 });
    shallowObj.value.count = 1; // 修改内部属性,不会触发更新
    triggerRef(shallowObj); // ✅ 手动强制触发更新
  • 重新赋值: 对于 ref,直接给 .value 赋予一个新对象是触发更新的最直接方式。

    const data = ref({ count: 0 });
    data.value = { ...data.value, count: data.value.count + 1 }; // ✅ 触发更新

通常,你应该优先依赖 Vue 的自动响应式系统,手动触发更新应作为最后的手段。

五、虚拟 DOM 与渲染优化

21. Vue3 的虚拟 DOM 有哪些改进?

Vue3 对虚拟 DOM (Virtual DOM) 进行了重写,带来了显著的性能提升:

  1. 静态提升 (Static Hoisting): 编译时,Vue3 会将模板中的静态节点提升到渲染函数之外。这意味着这些静态节点只在首次渲染时创建一次,后续更新时会直接复用,避免了不必要的创建和比对开销。
  2. Patch Flags (补丁标志): 编译时,Vue3 会对动态节点进行分析,并为它们打上不同的 PatchFlag(如 TEXT, PROPS, CLASS 等)。在 diff 过程中,Vue 可以根据这些标志精确地知道需要检查哪些内容,从而跳过大量不必要的对比。例如,一个节点只有 class 是动态的,那么更新时就只比对其 class,而忽略其子内容和属性。
  3. 树结构优化 (Tree Flattening): Vue3 会动态地记录包含动态节点的块(Block)。在更新时,它可以直接跳过这些块之外的纯静态内容,大大减少了需要遍历的节点数量。
  4. 事件缓存 (Cache Event Handlers): 内联的事件处理器(如 @click="handleClick")会被缓存起来,避免在每次渲染时重新创建函数,减少了内存开销和 GC 压力。

这些优化使得 Vue3 的 diff 算法比 Vue2 更加高效。

22. 什么是静态提升?

静态提升是 Vue3 编译阶段的一项重要优化。当编译器检测到模板中的纯静态内容(不包含任何动态绑定或指令的节点)时,会将这些静态节点的创建逻辑提升到渲染函数之外,成为一个常量。

效果:

  • 这些静态节点只在应用首次渲染时被创建一次。
  • 在后续的每次重新渲染中,它们都会被直接复用,而无需重新创建和比对。
  • 这节省了创建虚拟 DOM 节点的开销,也减少了内存使用。

示例: 假设模板如下:

<template>
  <div>
    <span>This is a static header</span> <!-- 静态节点 -->
    <p>{{ dynamicText }}</p> <!-- 动态节点 -->
  </div>
</template>

编译后的渲染函数大致如下:

// 静态节点被提升到渲染函数外部,只创建一次
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "This is a static header", -1 /* HOISTED */);

function render(_ctx, _cache) {
  return _openBlock(), _createBlock("div", null, [
    _hoisted_1, // 直接复用静态节点
    _createVNode("p", null, _toDisplayString(_ctx.dynamicText), 1 /* TEXT */)
  ]);
}

23. 什么是 PatchFlag?

PatchFlag 是编译时添加到虚拟节点 (VNode) 上的一个数字标志,用于标识这个 VNode 有哪些类型的动态绑定或需要更新的部分。

作用: 在运行时 diff 算法中,Vue 可以根据 PatchFlag 快速判断出需要为这个节点做哪些更新操作,从而跳过大量不必要的属性或内容比较,极大提升了 diff 效率。

常见的 PatchFlag 类型:

export const enum PatchFlags {
  TEXT = 1, // 动态文本内容
  CLASS = 1 << 1, // 动态 class
  STYLE = 1 << 2, // 动态 style
  PROPS = 1 << 3, // 动态属性 (不包括 class/style)
  FULL_PROPS = 1 << 4, // 表示需要完整 props 对比(例如 key 变化的节点)
  HYDRATE_EVENTS = 1 << 5, // 带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6, // 子节点顺序不会变化的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 带有 key 的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9, // 只需要非 props 的 patch
  DYNAMIC_SLOTS = 1 << 10, // 动态插槽
  HOISTED = -1, // 表示是静态提升的节点
  BAIL = -2 // 表示 diff 算法应该完全退出这个节点及其子树的优化模式
}

例如,一个节点只有 :class 是动态的,它会被标记为 CLASS | 2。在更新时,Vue 看到这个标志,就知道只需要检查它的 class 属性是否变化,而无需检查其子内容或其他属性。

24. 如何优化 Vue3 的渲染性能?

除了 Vue3 内置的优化,开发者也可以采取一些措施来提升应用性能:

  1. 合理使用 v-if 和 v-show:

    • v-if 是真正的条件渲染,切换时元素会被销毁/重建,适用于运行时条件很少改变的场景。
    • v-show 只是切换 CSS 的 display 属性,初始渲染开销大,但切换开销小,适用于需要频繁切换的场景。
  2. 使用 computed 和 watch 缓存: 充分利用 computed 的缓存特性,避免在模板中执行复杂计算或方法调用。

  3. 使用 v-once: 用于只渲染一次的元素或组件,之后跳过更新。

    <span v-once>This will never change: {{ staticMessage }}</span>
  4. 使用 v-memo: Vue3.2+ 新增指令,用于缓存一个模板的子树。只有当依赖数组中的值发生变化时,才会重新渲染。

    <div v-memo="[dependencyA, dependencyB]">
      <!-- 大量内容,只有当 dependencyA 或 dependencyB 变化时才会更新 -->
      {{ dependencyA }}
      {{ dependencyB }}
    </div>
  5. 列表渲染优化:

    • 始终提供唯一的 :key。
    • 对于超长列表,使用虚拟滚动技术(如 vue-virtual-scroller),只渲染可视区域内的元素。
  6. 减少不必要的响应式: 对于不需要响应式的数据,不要用 ref 或 reactive 包裹。对于大型深层嵌套对象,考虑使用 shallowRef 或 shallowReactive。

  7. 组件懒加载/异步组件: 使用 defineAsyncComponent 延迟加载非首屏必需的组件,减少初始包大小。

    import { defineAsyncComponent } from 'vue';
    const AsyncComp = defineAsyncComponent(() => import('./components/MyHeavyComponent.vue'));
  8. 使用 KeepAlive: 缓存非活动组件实例,避免重复渲染。

    <KeepAlive>
      <component :is="currentComponent"></component>
    </KeepAlive>

25. 什么是 v-memo 指令?

v-memo 是 Vue 3.2 中引入的一个指令,用于缓存一个模板片段的虚拟 DOM 子树。它接受一个依赖数组。只有当数组中的某个值发生变化时,才会重新渲染该子树;否则,将复用之前的虚拟 DOM。

使用场景: 主要用于优化渲染性能非常昂贵的组件或大型 v-for 列表,在依赖明确且变化不频繁时非常有效。

示例:

<template>
  <div v-for="user in users" :key="user.id" v-memo="[user.id, user.isActive]">
    <!-- 这个复杂的子树只有在 user.id 或 user.isActive 变化时才会更新 -->
    <p>{{ user.name }}</p>
    <p>{{ user.email }}</p>
    <p>{{ user.isActive ? 'Active' : 'Inactive' }}</p>
    <!-- ... 很多其他内容 -->
  </div>
</template>

在上例中,即使 users 数组的其他项发生了变化,或者父组件重新渲染,只要某个特定用户的 id 和 isActive 状态没变,其对应的 DOM 就会直接复用,避免了昂贵的虚拟 DOM 差异比对和真实 DOM 操作。

注意: 确保依赖数组包含所有在子模板中使用的、可能变化的响应式值。与 v-for 一起使用时,v-memo 需要放在同一个元素上。

六、组件与指令

26. Vue3 中的自定义指令有哪些变化?

Vue3 中的自定义指令的 API 与生命周期钩子更加一致,发生了一些变化:

Vue2 钩子Vue3 钩子说明
bindbeforeMount指令第一次绑定到元素时调用(Vue2的 bind 在 Vue3 中被 beforeMount 替代)
insertedmounted元素被插入到父 DOM 时调用。
update (已移除)-在 Vue3 中被移除。组件更新前钩子逻辑应放在 beforeUpdate 中。
componentUpdatedupdated所在组件及其子组件全部更新后调用。
unbindunmounted指令与元素解绑时调用。
-beforeUpdate (新增)在元素本身更新之前调用(类似组件的 beforeUpdate)。

Vue3 自定义指令示例:

// main.js
const app = createApp(App);

// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus();
  },
  // 在元素更新前调用(Vue3新增)
  beforeUpdate(el, binding) {
    // 可以在这里执行一些更新前的逻辑
  },
  // 组件更新后调用(Vue2的componentUpdated)
  updated(el, binding) {
    // ...
  },
  // 解绑时调用
  unmounted(el) {
    // 清理工作
  }
});
<!-- 在模板中使用 -->
<input v-focus />

binding 参数: 在钩子函数中接收,包含以下属性:

  • value: 传递给指令的值(如 v-my-directive="value")。
  • oldValue: 之前的值,仅在 beforeUpdate 和 updated 中可用。
  • arg: 传递给指令的参数(如 v-my-directive:foo 中的 foo)。
  • modifiers: 一个包含修饰符的对象(如 v-my-directive.foo.bar 中的 { foo: true, bar: true })。
  • instance: 使用指令的组件实例。
  • dir: 指令的定义对象。

27. 什么是 Teleport 组件?它的使用场景是什么?

Teleport 是 Vue3 内置的一个组件,它可以将其插槽内容渲染到 DOM 中的另一个指定位置,而不是在当前组件的位置。

使用场景: 最常见的场景是处理模态框 (Modal)、弹出层 (Popover)、提示框 (Toast)、加载条 (Loading) 等需要脱离当前组件布局流、通常需要定位到 body 或其他特定容器下的组件。这样可以避免父组件的 CSS 样式(如 z-index, overflow: hidden, transform)对弹出内容造成影响。

使用方法:

<template>
  <div>
    <button @click="showModal = true">Open Modal</button>
    <!-- 使用 teleport 将模态框内容渲染到 body 元素下 -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <p>This is a modal!</p>
        <button @click="showModal = false">Close</button>
      </div>
    </Teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const showModal = ref(false);
</script>

<style>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9999;
  /* ... 其他样式 */
}
</style>

to 属性接受一个 CSS 选择器字符串或一个实际的 DOM 元素。它确保模态框的 HTML 结构最终位于 body 的末尾,从而避免受到父组件布局的干扰。

28. 什么是 Suspense 组件?如何使用它来处理异步组件?

Suspense 是 Vue3 内置的一个组件,用于优雅地处理异步组件依赖的加载状态,在异步组件解析完成之前显示一个回退(fallback)内容。

使用场景:

  • 加载异步组件(通过 defineAsyncComponent 引入)。
  • 组件内部有异步 setup() 函数(返回一个 Promise)。
  • 配合 Composition API 的 async/await 进行数据获取。

使用方法:

<template>
  <Suspense>
    <!-- #default 插槽:渲染异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- #fallback 插槽:在异步组件加载完成前显示 -->
    <template #fallback>
      <div>Loading...</div> <!-- 可以是加载动画、骨架屏等 -->
    </template>
  </Suspense>
</template>

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

// 定义一个异步组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/MyAsyncComponent.vue')
);

export default {
  components: {
    AsyncComponent
  }
};
</script>

工作原理:

  1. <Suspense> 会等待其默认插槽内的所有异步依赖(异步组件或 async setup())都被解析。
  2. 在等待期间,它渲染 #fallback 插槽的内容。
  3. 一旦所有异步依赖都解析完成,它就渲染默认插槽的内容。

错误处理: <Suspense> 组件本身不处理错误。你需要使用 onErrorCaptured 生命周期钩子或 errorCaptured 选项来捕获和处理异步错误。

29. 如何在 Vue3 中创建异步组件?

在 Vue3 中,可以使用 defineAsyncComponent 方法来创建异步组件,实现组件的懒加载。

基本用法:

import { defineAsyncComponent } from 'vue';

// 1. 工厂函数返回 Promise
const AsyncComp = defineAsyncComponent(() => {
  return import('./components/MyAsyncComponent.vue'); // 动态 import 返回 Promise
});

// 2. 带有高级选项的对象形式
const AsyncCompWithOptions = defineAsyncComponent({
  // 加载函数
  loader: () => import('./MyAsyncComponent.vue'),
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
  delay: 200,
  // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
  timeout: 3000,
  // 定义组件是否可挂起 | 默认值:true
  suspensible: false,
  /**
   *
   * @param {*} error 错误信息对象
   * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
   * @param {*} fail 一个函数,指示加载程序结束退出
   * @param {*} attempts 允许的最大重试次数
   */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      // 重试错误,最多3次
      retry();
    } else {
      // 注意,retry/fail 就像 promise 的 resolve/reject 一样:
      // 必须调用其中一个才能继续错误处理。
      fail();
    }
  }
});

export default {
  components: {
    AsyncComp,
    AsyncCompWithOptions
  }
};

异步组件在打包时会被分割成单独的 chunk,只在需要时才会加载,有助于减小初始包体积,优化首屏加载速度。

30. 什么是递归组件?

递归组件是指组件在其自身模板中调用自身的组件。这在渲染具有嵌套或树形结构的数据时非常有用,例如评论列表、文件目录、级联选择器等。

在 Vue3 中使用递归组件:

  1. 全局组件: 全局注册的组件会自动在模板中可用,因此可以直接在自身模板中使用。
  2. 局部组件/<script setup>: 在 <script setup> 中,组件无法通过名称引用自身,必须通过导入自身来实现递归。

示例:一个简单的树形组件

<!-- TreeItem.vue -->
<template>
  <li>
    <div>{{ item.name }}</div>
    <ul v-if="item.children && item.children.length">
      <!-- 关键:组件导入自身并使用 -->
      <TreeItem
        v-for="child in item.children"
        :key="child.id"
        :item="child"
      />
    </ul>
  </li>
</template>

<script setup>
// 必须导入自身
import TreeItem from './TreeItem.vue';

defineProps({
  item: Object
});
</script>
<!-- ParentComponent.vue -->
<template>
  <ul>
    <TreeItem v-for="item in treeData" :key="item.id" :item="item" />
  </ul>
</template>

<script setup>
import TreeItem from './TreeItem.vue';

const treeData = [/* ... 树形数据 ... */];
</script>

注意事项:

  • 必须有一个明确的终止条件(如 v-if),否则会导致无限递归和栈溢出。
  • 确保每个递归实例都有一个唯一的 key。
  • 对于非常深的树,递归组件可能会带来性能考虑,此时可以考虑使用虚拟滚动等技术。

七、状态管理 (Pinia)

31. Pinia 和 Vuex 的区别是什么?

Pinia 是 Vue 官方推荐的下一代状态管理库,可以看作是 Vuex 5。它与 Vuex 4 的主要区别如下:

特性Vuex 4Pinia
API 风格Options API 风格Composition API 风格
mutations必须通过 mutations 同步修改状态没有 mutations,允许同步和异步直接修改状态
TypeScript 支持支持,但需要一些额外配置一流支持,提供出色的类型推断,API 设计非常 TypeScript 友好
模块化使用 modules使用多个独立的 store,扁平化结构,支持自动代码分割
Store 实例单一 store 实例多个 store 实例
体积稍大非常轻量,压缩后约 1KB
DevTools 支持支持支持

Pinia 的优势:

  • 更简洁的 API: 去除了 mutations 和 modules 的嵌套结构,学习成本和心智负担更低。
  • 更好的 TS 支持: 类型推断几乎开箱即用。
  • 更自然的组合: 使用 Composition API,可以像写组合式函数一样组织 store 的逻辑。
  • 与 Vue3 生态融合更好: 是 Vue 官方生态的一部分。

32. 如何在 Pinia 中定义一个 store?

在 Pinia 中,一个 store 是通过 defineStore 函数定义的。它需要一个唯一名称作为第一个参数和一个选项对象(或一个 Setup 函数)。

选项式风格 (类似 Vuex):

// stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  // 相当于 data
  state: () => ({
    count: 0
  }),
  // 相当于 computed
  getters: {
    doubleCount: (state) => state.count * 2
  },
  // 相当于 methods (可以同步或异步)
  actions: {
    increment() {
      this.count++;
    },
    async incrementAsync() {
      setTimeout(() => {
        this.increment();
      }, 1000);
    }
  }
});

组合式风格 (类似 Vue3 Setup):

// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0);
  // getters
  const doubleCount = computed(() => count.value * 2);
  // actions
  function increment() {
    count.value++;
  }
  function asyncIncrement() {
    setTimeout(() => {
      increment();
    }, 1000);
  }

  return { count, doubleCount, increment, asyncIncrement };
});

33. 如何在组件中使用 Pinia store?

首先,需要在应用层面安装 Pinia:

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

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

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

然后,在组件中导入并使用 store:

<!-- MyComponent.vue -->
<template>
  <div>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment()">Increment</button>
    <!-- 或者使用解构,但会失去响应性 -->
    <p>Count (destructured): {{ count }}</p>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia'; // 用于保持解构的响应性

const counterStore = useCounterStore();

// 直接解构会失去响应性!
// const { count, doubleCount } = counterStore; // ❌

// 使用 storeToRefs 可以保持响应性
const { count, doubleCount } = storeToRefs(counterStore); // ✅
</script>

Pinia 的 store 是一个 reactive 对象,所以可以直接访问其属性。要解构出 state 或 getters 并保持响应性,需要使用 storeToRefs()。

34. Pinia 如何实现持久化存储?

Pinia 本身不提供持久化功能。要实现状态持久化(如存入 localStorage),通常有两种方式:

  1. 手动在 store 中使用:

    // stores/counter.js
    import { defineStore } from 'pinia';
    
    export const useCounterStore = defineStore('counter', {
      state: () => ({
        count: localStorage.getItem('count') ? parseInt(localStorage.getItem('count')) : 0
      }),
      actions: {
        increment() {
          this.count++;
          // 每次修改后持久化
          localStorage.setItem('count', this.count);
        }
      }
    });
  2. 使用插件: 社区有成熟的插件如 pinia-plugin-persistedstate,可以更方便地实现。

    • 安装:npm install pinia-plugin-persistedstate
    • 使用:
    // main.js
    import { createPinia } from 'pinia';
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
    
    const pinia = createPinia();
    pinia.use(piniaPluginPersistedstate);
    // stores/counter.js
    export const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      persist: true // 开启持久化
    });

35. 如何在 Pinia 中处理异步操作?

在 Pinia 中,actions 既可以包含同步逻辑,也可以包含异步逻辑。处理异步操作非常简单自然。

示例:在 action 中发起异步请求

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

export const useUserStore = defineStore('user', {
  state: () => ({
    userData: null,
    loading: false,
    error: null
  }),
  actions: {
    async fetchUser(userId) {
      this.loading = true;
      this.error = null;
      try {
        const response = await fetch(`/api/users/${userId}`);
        this.userData = await response.json();
      } catch (error) {
        this.error = error.message;
      } finally {
        this.loading = false;
      }
    }
  }
});

在组件中调用:

<template>
  <div>
    <div v-if="userStore.loading">Loading...</div>
    <div v-else-if="userStore.error">Error: {{ userStore.error }}</div>
    <div v-else>{{ userStore.userData }}</div>
    <button @click="userStore.fetchUser(123)">Fetch User</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user';

const userStore = useUserStore();
// 也可以在 onMounted 中调用
</script>

Pinia 的异步 action 使得管理异步状态(加载中、错误、数据)变得非常直观,无需额外的中间件。

八、路由 (Vue Router 4)

36. Vue Router 4 有哪些主要变化?

Vue Router 4 是专为 Vue 3 设计的版本,其主要变化包括:

  • 创建方式: 使用 createRouter 函数而不是 new Router()。
  • 模式定义: mode: 'history' 改为 history: createWebHistory()(还有 createWebHashHistory 和 createMemoryHistory)。
  • 路由守卫: 导航守卫的 API 基本一致,但现在支持 Composition API 的 onBeforeRouteUpdate 和 onBeforeRouteLeave。
  • 移除: 移除了 *(星号或通配符)路由,必须使用自定义正则参数来捕获所有路由:/:catchAll(.*)。
  • <router-view> 和 <router-link>: 用法基本不变,但现在支持 v-slot API。
  • Composition API 支持: 提供了 useRouter, useRoute 等函数在 setup 中访问路由和当前路由信息。

37. 如何在新项目中使用 Vue Router 4?

安装与配置:

  1. 安装: npm install vue-router@4
  2. 创建路由器实例:
    // router/index.js
    import { createRouter, createWebHistory } from 'vue-router';
    import Home from '../views/Home.vue';
    
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home
      },
      {
        path: '/about',
        name: 'About',
        // 路由级代码分割,生成 about.[hash].js 块
        component: () => import('../views/About.vue')
      }
    ];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL), // 使用 history 模式
      routes
    });
    
    export default router;
  3. 在 main.js 中安装:
    // main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    
    const app = createApp(App);
    app.use(router);
    app.mount('#app');
  4. 在 App.vue 中使用:
    <!-- App.vue -->
    <template>
      <div id="app">
        <nav>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view />
      </div>
    </template>

38. 如何在 setup 中使用 Vue Router?

在 setup 函数中,你可以使用 Composition API 提供的 useRouter 和 useRoute 函数来访问路由实例和当前路由信息。

  • useRouter(): 返回路由器的实例,相当于选项式 API 中的 this.$router。用于编程式导航。
  • useRoute(): 返回一个当前路由地址的响应式对象,相当于选项式 API 中的 this.$route。包含 params, query, hash 等信息。

示例:

<!-- Component.vue -->
<template>
  <div>
    <p>Current Route Path: {{ route.path }}</p>
    <p>User ID: {{ route.params.id }}</p>
    <button @click="goToAbout">Go to About</button>
  </div>
</template>

<script setup>
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const route = useRoute(); // 这是一个响应式对象

const goToAbout = () => {
  // 编程式导航
  router.push('/about');
  // 或者 router.push({ name: 'About' });
};

// 监听路由参数变化
import { watch } from 'vue';
watch(() => route.params.id, (newId) => {
  // 对参数变化做出响应...
});
</script>

39. Vue Router 4 的路由守卫有哪些?如何使用?

Vue Router 4 提供了多种路由守卫,用于控制导航:

  1. 全局守卫 (在路由器实例上定义):

    • router.beforeEach: 全局前置守卫。
    • router.beforeResolve: 全局解析守卫。
    • router.afterEach: 全局后置钩子。
    // router/index.js
    router.beforeEach((to, from, next) => {
      // ...
      next(); // 必须调用 next() 来解析这个钩子
    });
  2. 路由独享的守卫 (在路由配置中定义):

    • beforeEnter
    const routes = [
      {
        path: '/admin',
        component: Admin,
        beforeEnter: (to, from, next) => {
          // 检查用户权限...
          next();
        }
      }
    ];
  3. 组件内的守卫 (在组件选项中或 setup 中使用):

    • onBeforeRouteUpdate: Composition API 钩子,在当前路由改变但组件被复用时调用。
    • onBeforeRouteLeave: Composition API 钩子,在导航离开该组件的对应路由时调用。
    • 选项式 API 的 beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave 仍然可用。
    <script setup>
    import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
    
    onBeforeRouteUpdate(async (to, from, next) => {
      // 在当前路由改变,但是该组件被复用时调用
      // 例如,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候
    });
    
    onBeforeRouteLeave((to, from, next) => {
      // 在导航离开渲染该组件的对应路由时调用
      const answer = window.confirm('Do you really want to leave?');
      if (answer) {
        next();
      } else {
        next(false);
      }
    });
    </script>

40. 如何实现路由懒加载?

路由懒加载是一种优化技术,它把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,从而减小初始包的体积。

在 Vue Router 4 中,使用动态 import 语法(也称为代码分割)来实现懒加载:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue') // 懒加载
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue') // 懒加载
  },
  {
    path: '/user/:id',
    name: 'User',
    // 将组件按组分块,共享同一个 chunk
    component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue')
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

注释 /* webpackChunkName: "home" */ 是 webpack 特有的语法,用于为生成的 chunk 指定名称。使用 Vite 时,通常不需要这样做,但写了也无害。

结合 <Suspense> 组件,可以为异步加载的组件提供更好的用户体验。

九、其他高级特性与最佳实践

好的,我将为您整理50道常考的Vue3相关面试题及其答案。以下是前35道题的答案:

41. 什么是渲染函数 (Render Function)?如何使用?

渲染函数是 Vue 组件的一种高级用法,它允许开发者使用 JavaScript 代码来生成虚拟 DOM 节点,而不是使用模板语法。渲染函数提供了更大的灵活性和程序化控制能力。

使用方法: 在 Vue3 中,渲染函数通过 setup() 函数返回一个函数来定义,使用 h() 函数(hyperscript)创建虚拟节点。

import { h } from 'vue';

export default {
  setup(props, context) {
    // 返回渲染函数
    return () => h('div', { class: 'container' }, [
      h('h1', 'Hello Vue 3'),
      h('p', `This is a render function example`),
      h('button', { 
        onClick: () => console.log('Clicked!'),
        class: 'btn'
      }, 'Click me')
    ]);
  }
}

使用场景:

  • 需要高度动态的组件结构
  • 实现复杂的逻辑控制
  • 与第三方库集成(如 D3.js)
  • 性能优化场景

42. h() 函数的参数有哪些?

h() 函数是创建虚拟 DOM 节点的核心函数,接受三个参数:

  1. type (必需):可以是字符串(HTML 标签名)、组件对象、异步组件函数或特殊组件(如 Fragment、Teleport)。
  2. props (可选):一个对象,包含节点的属性、样式、事件监听器等配置。
  3. children (可选):子节点,可以是字符串、数组或嵌套的 h() 调用。
// 基本用法
h('div', { class: 'container' }, 'Hello World')

// 带有事件和样式
h('button', {
  class: 'btn-primary',
  style: { color: 'red', fontSize: '14px' },
  onClick: () => console.log('Clicked')
}, 'Click me')

// 嵌套子元素
h('div', { class: 'container' }, [
  h('h1', 'Title'),
  h('p', 'Paragraph content')
])

43. 渲染函数与模板的对比

特性模板 (Template)渲染函数 (Render Function)
语法类 HTML 声明式语法JavaScript 编程式语法
学习曲线较平缓,易于上手较陡峭,需要 JavaScript 知识
灵活性受限,适用于常见场景极高,可处理复杂逻辑
性能Vue3 编译优化后性能良好手动优化可达到极致性能
类型支持有限更好的 TypeScript 支持
适用场景大多数常规 UI 开发动态组件、复杂逻辑、库开发

选择建议:

  • 对于大多数应用场景,推荐使用模板,因其更直观、易维护。
  • 只有在需要高度动态性、程序化控制或直接操作虚拟 DOM 的场景下,才考虑使用渲染函数。

44. 如何在渲染函数中实现条件渲染?

在渲染函数中,可以使用普通的 JavaScript 条件语句(如 if/else、三元运算符)来实现条件渲染。

import { h } from 'vue';

export default {
  setup(props) {
    return () => {
      // 条件逻辑
      if (props.isLoading) {
        return h('div', { class: 'loading' }, 'Loading...');
      } else if (props.error) {
        return h('div', { class: 'error' }, props.errorMessage);
      } else {
        return h('div', { class: 'content' }, [
          h('h1', props.title),
          h('p', props.content)
        ]);
      }
    };
  }
}

使用三元运算符:

return () => h('div', [
  h('h1', 'Title'),
  props.showSubtitle ? h('h2', 'Subtitle') : null,
  h('p', 'Content')
]);

45. 如何在渲染函数中实现列表渲染?

在渲染函数中,可以使用数组的 map() 方法来实现列表渲染。

import { h } from 'vue';

export default {
  setup(props) {
    return () => h('ul', 
      { class: 'item-list' },
      props.items.map(item => {
        return h('li', { 
          key: item.id, // 重要:为每个列表项提供唯一的 key
          class: 'item'
        }, item.name);
      })
    );
  }
}

带有复杂结构的列表:

return () => h('div', 
  { class: 'user-list' },
  props.users.map(user => {
    return h('div', { key: user.id, class: 'user-card' }, [
      h('img', { src: user.avatar, alt: user.name }),
      h('h3', user.name),
      h('p', user.email),
      h('button', {
        onClick: () => props.onUserSelect(user)
      }, 'Select')
    ]);
  })
);

46. 如何在渲染函数中处理事件?

在渲染函数中,可以通过 on 前缀来绑定事件处理函数。

import { h } from 'vue';

export default {
  setup(props, { emit }) {
    const handleClick = (event) => {
      console.log('Clicked', event);
      emit('button-clicked', event);
    };

    const handleInput = (event) => {
      emit('input-updated', event.target.value);
    };

    return () => h('div', [
      h('button', {
        onClick: handleClick,
        class: 'btn'
      }, 'Click me'),
      
      h('input', {
        onInput: handleInput,
        onFocus: () => console.log('Focused'),
        onBlur: () => console.log('Blurred'),
        value: props.value,
        type: 'text'
      })
    ]);
  }
}

事件修饰符的处理: 在渲染函数中,事件修饰符(如 .stop, .prevent)需要手动实现:

const handleClick = (event) => {
  // 手动实现 .stop.prevent
  event.stopPropagation();
  event.preventDefault();
  
  emit('custom-event', event);
};

return () => h('button', {
  onClick: handleClick
}, 'Click me');

47. 如何在渲染函数中使用插槽?

在渲染函数中,可以通过 context.slots 访问插槽内容。

import { h } from 'vue';

export default {
  setup(props, context) {
    return () => h('div', { class: 'card' }, [
      // 默认插槽
      context.slots.default ? context.slots.default() : h('p', 'Default content'),
      
      // 具名插槽
      context.slots.header ? context.slots.header() : null,
      
      // 作用域插槽
      context.slots.footer ? context.slots.footer({ 
        timestamp: new Date() 
      }) : null
    ]);
  }
}

在父组件中使用:

// 父组件
import { h } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  setup() {
    return () => h(ChildComponent, {
      // 通过 scopedSlots 传递作用域插槽
      scopedSlots: {
        default: () => h('p', 'Main content'),
        header: () => h('h1', 'Title'),
        footer: (props) => h('p', `Footer at ${props.timestamp}`)
      }
    });
  }
}

48. 如何在渲染函数中实现 v-model?

在渲染函数中,需要手动实现 v-model 的双向绑定逻辑。

import { h } from 'vue';

export default {
  setup(props, { emit }) {
    const handleInput = (event) => {
      // 手动触发 update:modelValue 事件
      emit('update:modelValue', event.target.value);
    };

    return () => h('input', {
      value: props.modelValue,
      onInput: handleInput,
      type: 'text'
    });
  }
}

使用计算属性实现复杂逻辑:

import { h, computed } from 'vue';

export default {
  setup(props, { emit }) {
    const inputValue = computed({
      get: () => props.modelValue,
      set: (value) => emit('update:modelValue', value)
    });

    const handleInput = (event) => {
      inputValue.value = event.target.value;
    };

    return () => h('input', {
      value: inputValue.value,
      onInput: handleInput,
      type: 'text'
    });
  }
}

49. 什么是 JSX?如何在 Vue3 中使用 JSX?

JSX 是一种 JavaScript 的语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构。在 Vue3 中使用 JSX 可以提供更直观的组件编写方式。

配置 JSX:

  1. 安装必要的依赖:
npm install @vitejs/plugin-vue-jsx -D
  1. 在 vite.config.js 中配置:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';

export default defineConfig({
  plugins: [vue(), vueJsx()]
});

使用 JSX:

import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const count = ref(0);
    
    const increment = () => {
      count.value++;
    };

    return () => (
      <div class="container">
        <h1>JSX in Vue 3</h1>
        <p>Count: {count.value}</p>
        <button onClick={increment}>Increment</button>
      </div>
    );
  }
});

JSX 与渲染函数的对比:

  • JSX 语法更接近模板,更易读易写
  • JSX 提供了更好的类型支持(配合 TypeScript)
  • JSX 在复杂组件中更有优势
  • 渲染函数更灵活,适合高度动态的场景

50. 渲染函数的性能优化策略有哪些?

使用渲染函数时,可以采取以下性能优化策略:

  1. 使用 shallowRef 处理大型数据:

    import { shallowRef } from 'vue';
    
    setup() {
      // 对于大型对象或数组,使用 shallowRef 减少响应式开销
      const largeData = shallowRef([]);
      
      const updateData = (newData) => {
        largeData.value = Object.freeze(newData); // 冻结对象防止意外修改
      };
      
      return { largeData, updateData };
    }
  2. 实现组件缓存:

    import { h, keepAlive, defineAsyncComponent } from 'vue';
    
    const AsyncComponent = defineAsyncComponent(() => 
      import('./AsyncComponent.vue')
    );
    
    setup() {
      return () => h(keepAlive, [
        h(AsyncComponent, { key: 'async-comp' })
      ]);
    }
  3. 使用记忆化 (Memoization):

    import { computed } from 'vue';
    
    setup(props) {
      // 使用 computed 缓存计算结果
      const expensiveValue = computed(() => {
        return props.data.map(item => complexCalculation(item));
      });
      
      return () => h('div', expensiveValue.value);
    }
  4. 分片渲染大规模数据:

    import { h, ref, onMounted } from 'vue';
    
    setup() {
      const visibleItems = ref([]);
      const allItems = ref([]); // 大规模数据集
      
      const chunkRender = (items, chunkSize = 50) => {
        let index = 0;
        
        const renderChunk = () => {
          if (index >= items.length) return;
          
          const chunk = items.slice(index, index + chunkSize);
          index += chunkSize;
          
          // 使用 requestAnimationFrame 分批渲染
          requestAnimationFrame(() => {
            visibleItems.value = [...visibleItems.value, ...chunk];
            renderChunk();
          });
        };
        
        renderChunk();
      };
      
      onMounted(() => {
        chunkRender(allItems.value);
      });
      
      return () => h('div', 
        visibleItems.value.map(item => h('div', { key: item.id }, item.name))
      );
    }
  5. 减少不必要的渲染:

    import { h, shallowRef, watchEffect } from 'vue';
    
    setup(props) {
      const optimizedData = shallowRef([]);
      
      watchEffect(() => {
        // 只在数据确实发生变化时更新
        if (hasChanged(optimizedData.value, props.data)) {
          optimizedData.value = Object.freeze(props.data);
        }
      });
      
      return () => h('div', 
        optimizedData.value.map(item => h('div', { key: item.id }, item.name))
      );
    }

这些优化策略可以帮助提升渲染函数的性能,特别是在处理大型数据集或复杂组件时。

总结

Vue3 的渲染函数提供了强大的灵活性和控制能力,适用于需要高度动态或程序化 UI 的场景。通过 h() 函数,开发者可以直接创建和操作虚拟 DOM,实现比模板语法更复杂的逻辑。

关键要点:

  1. 渲染函数通过 h() 函数创建虚拟节点,接受 type、props 和 children 三个参数。
  2. 在条件渲染和列表渲染中,使用标准的 JavaScript 控制流语句。
  3. 事件处理通过 on 前缀的属性实现,需要手动处理事件修饰符。
  4. 插槽通过 context.slots 访问和处理。
  5. v-model 需要手动实现双向绑定逻辑。
  6. JSX 提供了更直观的语法,需要在构建工具中配置相应的插件。
  7. 性能优化策略包括使用 shallowRef、组件缓存、记忆化和分片渲染等。
最后更新: 2025/9/18 18:20