您好!作为一名资深前端面试官和Vue.js专家,我为您精心准备了30道关于Vue 3的面试题,涵盖了从基础到进阶、从理论到实践的各个方面,并提供了详细的解析。希望这些题目能帮助您在面试中脱颖而出!
解析: 这是Vue 3最核心的变化之一。
Object.defineProperty() 实现。它通过遍历数据对象的每一个属性来劫持 getter 和 setter。
Vue.set 或 this.$set)。push, splice)。Proxy 和 Reflect 实现。它代理整个数据对象,而不是单个属性。
解析: Composition API (组合式API) 是Vue 3中引入的一组API,主要用于组件逻辑的组织和复用。
data, methods, computed, watch 等选项中。当需要复用某个逻辑时(如混入 Mixins),容易造成命名冲突、来源不明确,且不清楚混入的属性是来自哪个 Mixin。
// Options API 痛点示例:用户注册逻辑分散
export default {
data() { return { username: '', password: '', errors: [] }; },
methods: {
handleRegister() { /* ... */ },
validateForm() { /* ... */ }
},
computed: { isValid() { /* ... */ } },
watch: { username(newVal) { /* ... */ } }
}
setup 函数中,或进一步提取到可复用的“组合式函数”(Composables) 中。setup 函数中直接访问响应式状态和方法。
// Composition API 优势示例:用户注册逻辑集中
import { ref, computed, watch } from 'vue';
import { useValidation } from './composables/useValidation'; // 可复用逻辑
export default {
setup() {
const username = ref('');
const password = ref('');
const { errors, validateForm } = useValidation(username, password);
const isValid = computed(() => errors.value.length === 0);
watch(username, (newVal) => { /* ... */ });
const handleRegister = () => { /* ... */ };
return {
username, password, errors, isValid, handleRegister
};
}
}
ref() 和 reactive() 有什么区别?它们各自的适用场景是什么?解析: 它们都是用于创建响应式数据的API,但用法和适用场景不同。
ref():
<script> 中需要通过 .value 属性访问和修改其内部值。但在 <template> 中会自动解包,可以直接使用。{ value: xxx },然后将这个对象变为响应式的。count,一个用户ID userId,或者一个从外部获取的单个用户对象。
import { ref } from 'vue';
const count = ref(0); // 基本类型
const user = ref({ name: 'Alice' }); // 单个对象引用
console.log(count.value); // 0
console.log(user.value.name); // Alice
reactive():
.value。formData,一个包含多个商品的购物车 cartItems。
import { reactive } from 'vue';
const form = reactive({
name: 'Bob',
email: '[email protected]'
});
console.log(form.name); // Bob
form.name = 'Charlie'; // 直接修改属性
const list = reactive([1, 2, 3]);
list.push(4); // 直接修改数组
computed() 和 watch()/watchEffect() 有什么区别?如何选择使用?解析: 它们都用于处理响应式数据的变化,但侧重点和使用场景不同。
computed() (计算属性):
ref 对象,需要通过 .value 访问。
import { ref, computed } from 'vue';
const price = ref(10);
const quantity = ref(2);
const totalPrice = computed(() => price.value * quantity.value); // totalPrice有缓存
console.log(totalPrice.value); // 20
price.value = 12; // 触发重新计算
console.log(totalPrice.value); // 24
watch() (侦听器):
import { ref, watch } from 'vue';
const searchKeyword = ref('');
watch(searchKeyword, (newKeyword, oldKeyword) => {
console.log(`Keyword changed from ${oldKeyword} to ${newKeyword}. Fetching data...`);
// fetchData(newKeyword); // 模拟网络请求
});
searchKeyword.value = 'Vue'; // 触发watch
watchEffect():
import { ref, watchEffect } from 'vue';
const width = ref(100);
const height = ref(200);
watchEffect(() => {
console.log(`Component size is: ${width.value}x${height.value}`); // 自动收集width和height作为依赖
// updateCanvas(width.value, height.value);
});
width.value = 150; // 触发watchEffect
选择总结:
computed: 如果你需要派生一个新的响应式数据,并且这个数据会被模板多次使用,选择 computed 以利用其缓存。watch: 如果你需要根据特定响应式数据的变化来执行副作用(如异步请求、DOM操作等),并且需要访问新旧值,选择 watch。watchEffect: 如果你需要根据任何响应式数据的变化来执行副作用,并且希望副作用在创建时就立即执行一次,选择 watchEffect。<script setup> 是什么?它带来了哪些好处?解析: <script setup> 是Vue 3.2+ 版本中引入的语法糖,用于在使用 Composition API 的单文件组件 (SFC) 中简化代码编写。
<script> 标签上添加 setup 属性。return 响应式数据和方法。在 <script setup> 中声明的顶层绑定(包括变量、函数、import等)都会自动暴露给模板。import 和 register 组件。导入的组件可以直接在模板中使用。
<!-- 旧写法 -->
<script>
import { ref } from 'vue';
import MyChild from './MyChild.vue';
export default {
components: { MyChild },
setup() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
}
</script>
<!-- <script setup> 新写法 -->
<script setup>
import { ref } from 'vue';
import MyChild from './MyChild.vue'; // 自动可用
const count = ref(0);
const increment = () => count.value++;
</script>
<script setup> 做了优化,减少了运行时开销。解析: 组件间通信是Vue应用开发中非常重要的一部分。
defineProps (Composition API) 或 props 选项 (Options API) 接收。这是最常用的父子通信方式,遵循单向数据流。
<!-- Parent.vue -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from Parent');
</script>
<!-- ChildComponent.vue -->
<template>
<p>{{ message }}</p>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
message: String
});
</script>
defineEmits (Composition API) 或 emits / $emit (Options API) 触发自定义事件,父组件通过 v-on 监听子组件的事件。
<!-- ChildComponent.vue -->
<template>
<button @click="handleClick">Click Me</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emits = defineEmits(['childClick']);
const handleClick = () => {
emits('childClick', 'Data from child');
};
</script>
<!-- Parent.vue -->
<template>
<ChildComponent @childClick="handleChildClick" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
const handleChildClick = (data) => {
console.log('Child clicked with data:', data);
};
</script>
provide / inject (祖孙/跨级通信): 提供者组件 (祖先) 使用 provide 提供数据,后代组件使用 inject 注入数据。适用于层级较深,避免多层 Props 传递 (Prop Drilling) 的情况。
// Ancestor.vue
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('dark');
provide('appTheme', theme); // 提供 'appTheme'
return { theme };
}
};
// Grandchild.vue
import { inject } from 'vue';
export default {
setup() {
const theme = inject('appTheme'); // 注入 'appTheme'
console.log(theme.value); // 'dark'
return { theme };
}
};
mitt / tiny-emitter (事件总线): 在Vue 3中,不推荐使用 this.$emit 作为全局事件总线,因为这会使应用难以维护。如果确实需要非父子组件之间的任意通信,可以使用轻量级的第三方事件库(如 mitt 或 tiny-emitter)来创建事件总线。
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
// ComponentA.vue
import emitter from './eventBus';
emitter.emit('customEvent', 'Hello from A');
// ComponentB.vue
import emitter from './eventBus';
emitter.on('customEvent', (data) => {
console.log('Received from A:', data);
});
emitter.off('customEvent', callback); // 记得在unmounted时移除监听
$refs (父组件直接访问子组件实例): 父组件通过 ref 属性获取子组件的实例,直接调用子组件的方法或访问其属性。这是一种不推荐的方式,因为它打破了组件的封装性,增加了耦合度。应该优先使用 Props 和 Emits。
<!-- Parent.vue -->
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childRef = ref(null);
const callChildMethod = () => {
childRef.value.someChildMethod();
};
</script>
defineProps 和 defineEmits 在TypeScript中如何进行类型声明?解析: Vue 3 对 TypeScript 提供了原生且一流的支持。在 <script setup> 中,可以使用类型字面量或接口进行类型声明。
defineProps 的类型声明:
<script setup lang="ts">
defineProps<{
message: string;
count?: number; // 可选属性
items: string[];
user: { id: number; name: string };
}>();
</script>
<script setup lang="ts">
interface Props {
message: string;
count?: number;
items: string[];
user: { id: number; name: string };
}
const props = defineProps<Props>();
// 也可以通过 props.message 访问
</script>
defineEmits 的类型声明:
<script setup lang="ts">
const emits = defineEmits<{
(e: 'update:modelValue', value: string): void; // v-model 的类型
(e: 'submit', data: { id: number; name: string }): void;
(e: 'click'): void; // 无参数事件
}>();
// 使用 emits
emits('submit', { id: 1, name: 'Test' });
</script>
解析: Tree-shaking (摇树优化) 是一种通过移除未使用的代码来优化打包体积的技术。
import 和 export 声明在编译时就确定),构建工具(如Webpack、Rollup、Vite)能够在编译时识别出模块中哪些导出是“死代码”(Dead Code),即没有被任何地方引用到的代码,然后将其从最终的打包文件中剔除。ref, reactive, computed 等都是单独导出的函数。这意味着当你只使用 ref 而不使用 reactive 时,打包工具可以轻松地将 reactive 的相关代码移除。Teleport 组件是做什么用的?举例说明其适用场景。解析: <Teleport> 是 Vue 3 中引入的一个内置组件,它允许你将组件的部分模板内容移动到DOM中的另一个位置,而该组件本身的逻辑(如状态、Props、事件)仍然与它声明时的父组件保持关联。
to (必选): 指定目标DOM元素的选择器(如 '#app', 'body')或一个DOM元素引用。disabled (可选): 布尔值,当为 true 时,<Teleport> 会渲染其内容到其声明的位置,禁用传送功能。body 下),避免样式冲突、z-index问题以及父组件的CSS overflow: hidden 影响。
<!-- App.vue -->
<template>
<div id="app">
<!-- 应用内容 -->
<TheButton @click="showModal = true">Open Modal</TheButton>
</div>
<!-- 模态框组件,使用 Teleport 传送到 body 元素下 -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<h2>这是模态框</h2>
<p>模态框的内容。</p>
<button @click="showModal = false">Close</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue';
import TheButton from './TheButton.vue';
const showModal = ref(false);
</script>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
</style>
Suspense 组件是做什么用的?它如何改善用户体验?解析: <Suspense> 是 Vue 3 中引入的一个内置组件,用于处理异步组件的加载状态,并在异步组件加载完成前显示回退内容 (fallback),加载完成后显示组件本身。
async setup() 的组件)加载完成时,提供一个加载状态的回退内容。#default (默认插槽): 渲染异步组件内容。#fallback (回退插槽): 渲染异步组件加载时的占位内容。
<!-- App.vue -->
<template>
<div>
<h1>Welcome to My App</h1>
<Suspense>
<!-- #default 插槽显示异步加载的组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- #fallback 插槽在加载时显示 -->
<template #fallback>
<div>Loading Async Component...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
// 定义一个异步组件
const AsyncComponent = defineAsyncComponent(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve({
template: '<div>I am an async component, loaded after 2 seconds!</div>'
});
}, 2000); // 模拟2秒加载时间
});
});
</script>
解析: 在 Vue 3 中,全局配置和功能通过 createApp() 返回的应用程序实例进行管理,而不是像 Vue 2 那样直接修改全局的 Vue 构造函数。
app.component(name, component)app.directive(name, directive)app.use(plugin, options)。插件通常通过 plugin.install(app, options) 方法将功能添加到应用实例。app.mixin(mixin)。虽然仍然支持,但对于大部分用例,Vue 3 更推荐使用 Composition API 的组合式函数。app.config.globalProperties.someProperty = 'value'。这允许你在任何组件实例中通过 this.$someProperty 访问(仅在 Options API 中,Composition API 推荐使用 provide/inject)。
// main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 注册全局组件
app.component('GlobalHeader', {
template: '<h2>This is a Global Header</h2>'
});
// 注册全局指令
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value || 'blue';
}
});
// 添加全局属性
app.config.globalProperties.$appName = 'My Vue 3 App';
app.config.globalProperties.$log = console.log;
// 使用插件 (例如 Vue Router 或 Pinia)
// import router from './router';
// app.use(router);
app.mount('#app');
Fragment 是什么?它解决了什么问题?解析: Fragment (片段) 是 Vue 3 中的一个新特性,它允许组件的模板拥有多个根节点,而无需显式地包裹在一个额外的DOM元素中。
<template> 必须有一个唯一的根元素,例如 <div> 或 <section>。这经常导致在DOM中生成不必要的嵌套层级。
<!-- Vue 2 必须包裹一个根元素 -->
<template>
<div>
<p>Hello</p>
<p>World</p>
</div>
</template>
<!-- Vue 3 允许多个根节点 (Fragment) -->
<template>
<p>Hello</p>
<p>World</p>
<button>Click me</button>
</template>
<KeepAlive> 组件有什么作用?它的生命周期钩子有哪些?解析: <KeepAlive> 是 Vue 3 中一个内置的抽象组件,用于缓存不活跃的组件实例,而不是销毁它们。这可以提高组件的性能,特别是在组件频繁切换的场景下。
<KeepAlive> 中的组件切换时,它们不会被销毁和重建,而是被缓存起来。当再次访问该组件时,可以直接从缓存中读取,避免了重复的渲染和销毁开销,从而提高用户体验。常用于路由组件的缓存。include (String, RegExp, Array): 只有名称匹配的组件会被缓存。exclude (String, RegExp, Array): 名称匹配的组件不会被缓存。max (Number): 最多可以缓存多少个组件实例。<KeepAlive> 缓存的组件,在切换时不会触发 unmounted 和 mounted 钩子,而是会触发两个特有的生命周期钩子:
onActivated(): 当组件被激活(从缓存中取出并显示)时调用。onDeactivated(): 当组件被停用(从视图中移除并放入缓存)时调用。
<!-- App.vue (配合 Vue Router) -->
<template>
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
<KeepAlive>
<router-view></router-view> <!-- 路由组件会被缓存 -->
</KeepAlive>
</template>
<!-- Home.vue (被缓存的组件) -->
<script setup>
import { onActivated, onDeactivated, ref } from 'vue';
const count = ref(0);
onActivated(() => {
console.log('Home component activated!');
});
onDeactivated(() => {
console.log('Home component deactivated!');
});
</script>
<template>
<h3>Home Page</h3>
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</template>
Custom Renderer 是什么?它有什么实际应用?解析: Vue 3 提供了 @vue/runtime-core 包中的 createRenderer API,允许开发者创建自定义渲染器 (Custom Renderer)。
// 简化示例,实际会更复杂
import { createRenderer } from 'vue';
const customRenderer = createRenderer({
// 实现一系列与目标平台交互的函数
createElement(tag) {
console.log(`Creating element: ${tag} on custom canvas`);
// 返回一个 Canvas 元素的模拟对象
return { tag, children: [] };
},
insert(el, parent, anchor) {
console.log(`Inserting ${el.tag} into ${parent.tag}`);
// 在 Canvas 上绘制或添加到其子节点
parent.children.push(el);
},
patchProp(el, key, prevValue, nextValue) {
console.log(`Patching prop ${key} for ${el.tag}: ${prevValue} -> ${nextValue}`);
el[key] = nextValue;
},
remove(el) {
console.log(`Removing ${el.tag}`);
// 从 Canvas 中移除
},
createText(text) {
console.log(`Creating text node: ${text}`);
return { type: 'text', value: text };
},
// ... 还有其他很多方法需要实现
});
// 创建一个自定义应用
const customApp = customRenderer.createApp({
data() {
return { count: 0 };
},
template: '<MyRect :x="count" />', // 假设 MyRect 是一个自定义组件
components: {
MyRect: {
props: ['x'],
template: '<rect :x="x" :y="10" :width="50" :height="50" />' // 自定义元素
}
}
});
// 挂载到自定义的“根”节点 (例如一个Canvas上下文)
customApp.mount(document.getElementById('myCanvas'));
Vite?它相比 Webpack 有哪些优势?为什么 Vue 3 推荐使用 Vite?解析: Vite 是一个由 Vue.js 作者尤雨溪开发的下一代前端构建工具,旨在解决现代前端开发中缓慢的开发服务器启动和缓慢的热更新 (HMR) 问题。
.vue 文件)进行处理。主要体现在开发体验上:
| 特性 | Vite | Webpack |
|---|---|---|
| 开发服务器启动 | 极快(几十到几百毫秒) | 相对较慢(几秒到几十秒,取决于项目大小) |
| 热更新 (HMR) | 秒级响应 | 相对较慢,尤其在大型项目下 |
| 模块处理 | 开发模式下使用 ESM 原生支持,按需加载;生产环境 Rollup 打包 | 开发和生产都需要打包器处理所有模块 |
| 配置复杂度 | 通常更简单,开箱即用 | 配置复杂,学习曲线陡峭,需要大量 loader/plugin |
解析: Vue 的渲染过程主要涉及模板编译、生成虚拟DOM (VNode)、Diffing算法和打补丁 (Patching) 更新真实DOM。
v-if, v-for, 动态绑定等)所在的节点标记为“块”,形成一个“块树”。在更新时,Diffing 算法只需要遍历块树,而不需要遍历整个虚拟DOM树,极大地减少了比较的开销。key 值匹配,会进行位置移动。v-for 列表)的移动操作。这使得在处理元素顺序变化时,能以最小的DOM操作(只移动确实需要移动的元素)来更新。总结: Vue 3 的渲染性能优化主要体现在编译时和运行时两个层面。编译时进行更智能的分析和优化,减少了运行时VNode的创建和比较开销;运行时则通过更高效的Diffing算法和Patch Flags实现精准的更新。这使得 Vue 3 在处理复杂组件和大规模数据时,性能表现更优。
解析: 在 Vue 3 的 Composition API 中,获取组件实例和模板引用都通过 ref 函数实现。
<script setup> 或 setup() 函数中声明一个 ref 变量,并初始化为 null。ref 变量名绑定到需要获取的DOM元素或子组件上,使用 ref="变量名"。onMounted 钩子中),该 ref 变量的 .value 属性就会指向对应的DOM元素或组件实例。
<template>
<!-- 获取DOM元素的引用 -->
<input type="text" ref="inputRef" />
<button @click="focusInput">Focus Input</button>
<!-- 获取子组件实例的引用 -->
<ChildComponent ref="childRef" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
const inputRef = ref(null);
const childRef = ref(null);
onMounted(() => {
// 访问DOM元素
if (inputRef.value) {
console.log('Input element:', inputRef.value);
}
// 访问子组件实例 (前提是子组件通过 defineExpose 暴露了方法)
if (childRef.value) {
console.log('Child component instance:', childRef.value);
}
});
const focusInput = () => {
inputRef.value.focus();
};
const callChildMethod = () => {
if (childRef.value && typeof childRef.value.someMethod === 'function') {
childRef.value.someMethod();
}
};
</script>
defineExpose):
如果父组件需要通过 $refs 访问子组件的内部方法或属性,子组件必须使用 defineExpose 显式地暴露它们。
<!-- ChildComponent.vue -->
<script setup>
import { ref, defineExpose } from 'vue';
const internalData = ref('Internal Data');
const someMethod = () => {
console.log('Child method called!', internalData.value);
};
// 显式暴露 internalData 和 someMethod 给父组件的 $refs
defineExpose({
internalData,
someMethod
});
</script>
<template>
<div>Child Component Content</div>
</template>
解析: 在 Vue 3 中,推荐使用 Pinia 作为官方的全局状态管理库。它旨在替代 Vuex 并在 Composition API 环境下提供更简洁、更直观的API。
npm install pinia 或 yarn add pinia
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia(); // 创建 Pinia 实例
app.use(pinia); // 注册 Pinia 插件
app.mount('#app');
// src/stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', { // 'counter' 是唯一的 ID
state: () => ({ // 状态
count: 0,
name: 'Eduardo',
}),
getters: { // 计算属性
doubleCount: (state) => state.count * 2,
doubleCountPlusOne(): number { // 也可以是函数,访问this
return this.doubleCount + 1;
},
},
actions: { // 方法
increment() {
this.count++;
},
async fetchData() {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
this.count += 5;
},
},
});
<!-- MyComponent.vue -->
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.fetchData">Fetch Data</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter';
import { storeToRefs } from 'pinia'; // 用于解构state和getter并保持响应性
const counterStore = useCounterStore();
// 如果需要解构 state 或 getter 属性并保持响应性,使用 storeToRefs
// const { count, doubleCount } = storeToRefs(counterStore);
// count.value++; // 这样修改count会触发响应式,但直接修改state.count更常见
// actions 可以直接调用
// counterStore.increment();
</script>
解析: Vue Router 是 Vue 官方的路由管理器,用于构建单页面应用 (SPA)。
npm install vue-router@4 或 yarn add vue-router@4
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About },
{ path: '/user/:id', name: 'UserDetail', component: () => import('../views/User.vue'), props: true } // 路由懒加载
];
const router = createRouter({
history: createWebHistory(), // HTML5 History 模式
// history: createWebHashHistory(), // Hash 模式
routes
});
export default router;
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
<!-- App.vue -->
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link :to="{ name: 'UserDetail', params: { id: 123 }}">User 123</router-link>
</nav>
<router-view></router-view> <!-- 路由组件的渲染出口 -->
</template>
Vue Router 提供了多种导航守卫,允许你在路由跳转的各个阶段执行逻辑,例如权限控制、登录验证、数据加载等。
router.beforeEach((to, from, next) => { ... })
to: 即将进入的目标路由对象。from: 当前导航正要离开的路由对象。next: 必须调用以解决钩子。
next(): 继续导航。next(false): 取消当前导航。next('/') 或 next({ path: '/' }): 重定向到其他路由。
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
// 如果需要登录但未登录,重定向到登录页
return { name: 'Login', query: { redirect: to.fullPath } };
}
});
router.beforeResolve((to, from) => { ... })
router.afterEach((to, from, failure) => { ... })
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: (to, from) => {
// 只有在满足条件时才进入 /admin
// return isAuthenticated();
}
}
];
beforeRouteEnter(to, from, next): 在路由进入组件前调用。此时组件实例尚未创建,无法访问 this。beforeRouteUpdate(to, from, next): 在当前路由改变,但该组件被复用时调用 (如动态路由参数变化)。beforeRouteLeave(to, from, next): 在导航离开组件的路由时调用。常用于提示用户保存未提交的更改。
// MyComponent.vue
import { onBeforeRouteLeave } from 'vue-router'; // Vue 3 Composition API
export default {
// Options API
// beforeRouteLeave(to, from) {
// const answer = window.confirm('Do you really want to leave? you have unsaved changes!');
// if (!answer) return false; // 阻止离开
// },
// Composition API
setup() {
const hasUnsavedChanges = true; // 假设有未保存的更改
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!');
if (!answer) return false;
}
});
}
};
解析: 直接对 reactive 对象进行解构会丢失响应性,因为解构出的变量不再是原始对象的代理。Vue 3 提供了 toRefs 和 toRef 函数来解决这个问题。
import { reactive } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
let { count, name } = state; // 解构
count++; // count 变量的值改变,但 state.count 保持不变,视图不更新
console.log(state.count); // 0
toRefs()
将一个响应式对象的所有属性转换为响应式引用 (ref) 对象。这样,解构这些 ref 就可以在不丢失响应性的前提下访问原始对象的属性。
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
const stateAsRefs = toRefs(state); // { count: Ref<number>, name: Ref<string> }
let { count, name } = stateAsRefs; // 解构出 ref 对象
count.value++; // 这样修改 count.value 会更新 state.count,并触发视图更新
console.log(state.count); // 1
console.log(count.value); // 1
name.value = 'Vue 3'; // 同样会更新 state.name
console.log(state.name); // Vue 3
适用场景: 当你希望将一个响应式对象的多个属性解构到独立的变量中,并且这些变量需要保持与原对象属性的响应式关联时。
toRef()
用于为响应式对象上的某个属性创建一个单独的响应式引用 (ref)。当你只想将对象中的某一个属性转换为 ref 而不是所有属性时使用。
import { reactive, toRef } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
const countRef = toRef(state, 'count'); // 创建一个指向 state.count 的 ref
countRef.value++; // 修改 countRef.value 会同时修改 state.count
console.log(state.count); // 1
console.log(countRef.value); // 1
// 如果需要获取一个不存在的属性的ref,toRef会返回一个可写的ref,但不会与原始对象关联
const newPropRef = toRef(state, 'newProp');
newPropRef.value = 'hello';
console.log(state.newProp); // undefined (因为原始对象没有这个属性)
适用场景: 当你只需要将响应式对象的某个特定属性包装成 ref,通常用于将响应式对象的单个属性作为 props 传递给子组件,同时保持响应性。
<Transition> 组件是如何实现过渡动画的?解析: <Transition> 是 Vue 3 中内置的组件,用于在元素或组件插入、更新或从DOM中移除时应用过渡动画。它结合 CSS 过渡和动画类名以及 JavaScript 钩子来实现动画效果。
当 <Transition> 包裹的元素或组件发生“进入”或“离开”的DOM操作时,Vue 会自动在不同的阶段添加/移除特定的 CSS 类名,从而触发 CSS 过渡或动画。
这些类名包括:
v-enter-from:进入过渡的开始状态,元素在进入前一帧的样式。v-enter-active:进入过渡的活跃状态,定义过渡的持续时间、延迟和曲线。v-enter-to:进入过渡的结束状态,元素在进入完成后一帧的样式。v-leave-from:离开过渡的开始状态,元素在离开前一帧的样式。v-leave-active:离开过渡的活跃状态,定义过渡的持续时间、延迟和曲线。v-leave-to:离开过渡的结束状态,元素在离开完成后一帧的样式。其中,v- 是默认前缀,可以通过 name 属性自定义,例如 <transition name="fade"> 对应的类名就是 fade-enter-from 等。
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello, Vue Transition!</p>
</Transition>
</template>
<script setup>
import { ref } from 'vue';
const show = ref(true);
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
除了 CSS 类名,<Transition> 也提供了 JavaScript 钩子,可以在过渡的不同阶段执行自定义的JavaScript动画逻辑,这对于结合第三方动画库(如 GSAP)或进行更复杂动画时非常有用。
@before-enter, @enter, @after-enter@enter-cancelled@before-leave, @leave, @after-leave@leave-cancelled
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@leave="onLeave"
>
<div v-if="show">I'm animated by JS!</div>
</Transition>
</template>
<script setup>
import { ref } from 'vue';
const show = ref(true);
const onBeforeEnter = (el) => {
el.style.opacity = 0;
el.style.transform = 'translateX(-100px)';
};
const onEnter = (el, done) => {
el.offsetWidth; // 强制 reflow
el.style.transition = 'opacity 0.5s, transform 0.5s';
el.style.opacity = 1;
el.style.transform = 'translateX(0)';
el.addEventListener('transitionend', done); // 告知 Vue 动画完成
};
const onLeave = (el, done) => {
el.style.transition = 'opacity 0.5s, transform 0.5s';
el.style.opacity = 0;
el.style.transform = 'translateX(100px)';
el.addEventListener('transitionend', done);
};
</script>
解析: Vue 3 提供了多种机制来捕获和处理应用中的错误,以提高应用的健壮性。
app.config.errorHandler
可以在应用级别捕获所有来自组件内部的未捕获错误(包括生命周期钩子、事件处理器、watch 和 computed 中的错误)。
// main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.config.errorHandler = (err, vm, info) => {
// `err`: 错误对象
// `vm`: 发生错误的组件实例
// `info`: Vue 特定的错误信息 (例如,发生在哪个生命周期钩子)
console.error('全局错误捕获:', err, vm, info);
// 可以在这里上报错误到监控系统,或者显示错误提示给用户
};
app.mount('#app');
onErrorCaptured()
这是一个 Composition API 的生命周期钩子,可以在组件内部捕获其所有子孙组件中发生的错误。它类似于 React 的错误边界。
// ParentComponent.vue
<template>
<div>
<h3>Parent Component</h3>
<ErrorProneChild />
</div>
</template>
<script setup>
import { onErrorCaptured, ref } from 'vue';
import ErrorProneChild from './ErrorProneChild.vue';
const error = ref(null);
onErrorCaptured((err, instance, info) => {
error.value = err;
console.error('子组件错误捕获:', err, instance, info);
// 返回 false 阻止错误继续向上传播(不再被全局错误处理器或上层onErrorCaptured捕获)
// return false;
});
</script>
// ErrorProneChild.vue
<template>
<div>
<button @click="triggerError">Trigger Error</button>
</div>
</template>
<script setup>
const triggerError = () => {
throw new Error('This is a simulated error from child component!');
};
</script>
try...catch
对于异步操作(如网络请求、定时器回调),Vue 的错误处理机制无法直接捕获,需要使用标准的 JavaScript try...catch 块。
// In a component method or setup function
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch data error:', error);
// 可以显示用户友好的错误信息
}
}
Suspense 和 Teleport 的组合使用场景。解析: Suspense 用于处理异步内容的加载状态,而 Teleport 用于将内容渲染到 DOM 树的不同位置。它们可以结合使用来创建更优雅的用户体验。
当一个模态框 (Modal) 或抽屉 (Drawer) 的内容是异步加载的,并且你希望在内容加载时显示一个加载动画,同时又希望模态框本身渲染在 body 元素下以避免层级问题。
<!-- App.vue -->
<template>
<button @click="showModal = true">Open Async Modal</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
<div class="modal-content">
<button class="close-button" @click="showModal = false">×</button>
<Suspense>
<template #default>
<!-- 异步加载的模态框内容组件 -->
<AsyncModalContent @close="showModal = false" />
</template>
<template #fallback>
<div class="modal-loading">Loading modal content...</div>
</template>
</Suspense>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const showModal = ref(false);
// 模拟一个异步加载的模态框内容组件
const AsyncModalContent = defineAsyncComponent(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve({
template: `
<div>
<h3>Async Loaded Modal Content</h3>
<p>This content took 2 seconds to load.</p>
<button @click="$emit('close')">Close from inside</button>
</div>
`,
emits: ['close']
});
}, 2000);
});
});
</script>
<style>
/* Modal Overlay and Content styles (同前一个Teleport示例) */
.modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 1000;
}
.modal-content {
background: white; padding: 25px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
min-width: 300px; max-width: 80%; position: relative;
}
.modal-loading {
padding: 30px; text-align: center; color: #555;
}
.close-button {
position: absolute; top: 10px; right: 10px; font-size: 24px;
background: none; border: none; cursor: pointer;
}
</style>
v-memo 指令是做什么用的?何时使用?解析: v-memo 是 Vue 3.2+ 中引入的一个性能优化指令,它允许你记忆化 (memoize) 一个模板的子树,只有当其依赖项发生变化时才重新渲染该子树。
v-memo 接收一个依赖数组。Vue 会在组件更新时,比较这个数组中的每个值。如果数组中的所有值都与上一次渲染时相同,那么这个元素及其所有子元素将完全跳过更新,包括虚拟DOM的Diffing过程。v-memo="[]" 来完全跳过它们的更新。v-memo 优化。v-memo。
<template>
<div>
<h3>Count: {{ count }}</h3>
<button @click="count++">Increment Count</button>
<h4>Static Content (with v-memo)</h4>
<!-- 只有当 count 变化时,这个 div 及其子内容才会被重新渲染 -->
<div v-memo="[count]">
<p>This paragraph will only re-render if count changes. Current count: {{ count }}</p>
<!-- 即使 time 变化,只要 count 没变,这里也不会更新 -->
<p>Current Time (might not update if count stable): {{ currentTime }}</p>
</div>
<h4>Truly Static Content (memoize once)</h4>
<!-- 传入空数组,表示该元素及其子树永远不会更新 -->
<div v-memo="[]">
<p>This content is completely static and will only render once.</p>
<p>It will not re-render even if count or currentTime changes.</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const count = ref(0);
const currentTime = ref('');
let timer;
onMounted(() => {
timer = setInterval(() => {
currentTime.value = new Date().toLocaleTimeString();
}, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
</script>
v-memo 通常用于微优化,在绝大多数情况下,Vue 的默认渲染效率已经足够高。v-memo 可能会导致代码可读性下降,并且需要仔细管理其依赖数组,避免引入错误。v-memo 的效果可能不明显,甚至可能因为依赖比较的开销而略微降低性能。解析: Vue 3 在 SSR 方面进行了多项底层改进,使得开发体验和性能都有所提升。
// 伪代码:Vue 3 流式 SSR
import { renderToNodeStream } from '@vue/server-renderer';
import { createApp } from 'vue';
const app = createApp({ /* ... */ });
const stream = renderToNodeStream(app); // 返回一个 Node.js 可读流
stream.pipe(res); // 直接管道到响应
onErrorCaptured 也可以在 SSR 上下文中工作。总结: Vue 3 的 SSR 改进主要集中在性能优化(更快的水合和流式渲染)和开发体验优化(更小的包体积、更好的TS支持、更一致的API),使得构建高性能的同构应用变得更加容易和高效。
解析: 除了 Pinia/Vuex 这种专业的集中式状态管理方案,Vue 3 也提供了其他几种方式来实现全局或跨组件的数据共享。
provide / inject:
provide 提供一个值,后代组件通过 inject 注入这个值。无论组件层级多深,都可以直接通信。ref 或 reactive 对象。
// Ancestor.vue
import { provide, ref } from 'vue';
setup() {
const count = ref(0);
provide('count_key', count); // 提供了响应式的ref
// 或者 provide('config_key', { apiBaseUrl: '...', theme: 'dark' });
return { count };
}
// Descendant.vue
import { inject } from 'vue';
setup() {
const count = inject('count_key'); // 接收到响应式的ref
console.log(count.value); // 可以访问和修改 .value
return { count };
}
ref 或 reactive 创建响应式数据,并导出相关方法。在需要使用的组件中直接导入并使用。
// src/composables/useGlobalState.js
import { ref, computed } from 'vue';
const globalCount = ref(0); // 声明一个全局响应式状态
const globalMessage = ref('Initial Message');
export function useGlobalState() {
const doubleGlobalCount = computed(() => globalCount.value * 2);
function incrementGlobalCount() {
globalCount.value++;
}
function setGlobalMessage(msg) {
globalMessage.value = msg;
}
return {
globalCount,
globalMessage,
doubleGlobalCount,
incrementGlobalCount,
setGlobalMessage,
};
}
// ComponentA.vue
<script setup>
import { useGlobalState } from '../composables/useGlobalState';
const { globalCount, incrementGlobalCount } = useGlobalState();
</script>
<template>
<p>Global Count: {{ globalCount }}</p>
<button @click="incrementGlobalCount">Increment Global</button>
</template>
// ComponentB.vue
<script setup>
import { useGlobalState } from '../composables/useGlobalState';
const { globalMessage, setGlobalMessage } = useGlobalState();
</script>
<template>
<p>Global Message: {{ globalMessage }}</p>
<button @click="setGlobalMessage('New Message')">Set New Message</button>
</template>
mitt, tiny-emitter) 作为事件中心,允许组件通过发布/订阅模式进行通信。this.$emit 作为全局事件总线。provide/inject 或直接的组合式函数。v-model 在组件上使用时有什么变化?如何自定义 v-model?解析: Vue 3 改进了 v-model 在组件上的用法,使其更加灵活和可定制。
v-model 默认行为:
在 Vue 2 中,v-model 在组件上等价于绑定 value Prop 和监听 input 事件。
<!-- Vue 2 等价于 -->
<MyInput :value="searchText" @input="searchText = $event.target.value" />
<!-- MyInput.vue (Vue 2) -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value']
};
</script>
v-model 默认行为:
在 Vue 3 中,v-model 在组件上等价于绑定 modelValue Prop 和监听 update:modelValue 事件。
<!-- Vue 3 等价于 -->
<MyInput :modelValue="searchText" @update:modelValue="searchText = $event" />
<!-- MyInput.vue (Vue 3) -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
modelValue: String // Prop 名改为 modelValue
});
defineEmits(['update:modelValue']); // 事件名改为 update:modelValue
</script>
v-model (多参数 v-model):
Vue 3 允许在一个组件上使用多个 v-model 绑定,每个绑定可以指定不同的属性名。
v-model:propName="data"propName 的 prop。update:propName 的事件。
<!-- Parent.vue -->
<template>
<CustomForm
v-model:title="pageTitle"
v-model:content="pageContent"
/>
<p>Title: {{ pageTitle }}</p>
<p>Content: {{ pageContent }}</p>
</template>
<script setup>
import { ref } from 'vue';
import CustomForm from './CustomForm.vue';
const pageTitle = ref('Default Title');
const pageContent = ref('Default Content');
</script>
<!-- CustomForm.vue -->
<template>
<div>
<label>Title:</label>
<input :value="title" @input="$emit('update:title', $event.target.value)" />
<br />
<label>Content:</label>
<textarea :value="content" @input="$emit('update:content', $event.target.value)"></textarea>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
title: String,
content: String
});
defineEmits(['update:title', 'update:content']);
</script>
解析: 渲染函数是 Vue 中一种更底层的创建组件视图的方式,它提供了比模板更强大的编程灵活性。Vue 的模板在内部也会被编译成渲染函数。
渲染函数是一个返回 虚拟DOM (VNode) 的函数。VNode 是一个普通的 JavaScript 对象,它描述了DOM元素的结构和属性。
// 这是一个简单的渲染函数,返回一个 h1 标签的 VNode
import { h } from 'vue';
export default {
render() {
return h('h1', 'Hello Render Function!');
}
};
在 Composition API 中,你可以从 setup() 函数返回一个渲染函数。这允许你结合响应式API来构建动态的、程序化的组件。
import { h, ref } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
// 返回一个渲染函数
return () =>
h('div', [
h('h1', `Count: ${count.value}`),
h('button', { onClick: increment }, 'Increment')
]);
}
};
h() 函数:
h() 是 createVNode() 的别名,用于创建 VNode。它接收三个参数:
'div', 'span'),组件选项对象,或组件本身。slots 对象更灵活地处理插槽内容。emits 选项/defineEmits 的作用和优势。解析: emits 选项 (Options API) 或 defineEmits (Composition API) 是 Vue 3 中用于显式声明组件可以发出的自定义事件的功能。
emits 可以防止自定义事件被错误地作为原生 DOM 事件监听器透传。emits 数组或对象。
export default {
emits: ['click', 'update:modelValue'], // 简单声明
// 或者带校验
// emits: {
// click: null, // 不带校验
// 'update:modelValue': (payload) => { // 带校验
// return typeof payload === 'string';
// }
// },
methods: {
handleClick() {
this.$emit('click');
}
}
}
<script setup>): 使用 defineEmits 宏。
<script setup lang="ts">
import { defineEmits } from 'vue';
// 简单声明
const emits = defineEmits(['click', 'update:modelValue']);
// 或者带类型校验 (推荐在 TypeScript 中使用)
// const emits = defineEmits<{
// (e: 'click', id: number): void;
// (e: 'update:modelValue', value: string): void;
// }>();
const handleClick = () => {
emits('click', 123); // 如果定义了类型,这里会进行类型检查
};
</script>
解析: 抱歉,这是一个重复的问题。前面已经详细讲解了除了 Pinia/Vuex 之外的两种主要方式:provide / inject 和可组合函数 (Composables) + 响应式API。
这里再简单总结一下,并强调其适用场景:
provide / inject:
ref 或 reactive 创建响应式状态,并导出操作这些状态的函数。this.$emit 作为全局事件总线。总结:
provide / inject 是一个优雅的解决方案。