1. 前端模块化
一、前端模块化的核心价值
前端模块化是将复杂项目拆分为独立功能单元(模块)的开发模式,通过明确定义的接口协作,解决以下痛点:
命名冲突:避免全局作用域下变量/函数名重复(如多个utils.js中的同名方法)。
依赖混乱:手动管理脚本加载顺序易出错(如A依赖B,需先加载B再加载A)。
维护困难:大型项目无法快速定位修改影响范围(模块职责不清晰时尤其明显)。
复用低效:通用功能(如请求工具、日期格式化)难以跨项目复用。
二、前端模块化的发展与标准

三、ES6 Modules 核心详解
ES6 Modules 是当前前端模块化的事实标准,支持在浏览器(通过
- 导出(Export)
模块通过export暴露成员,分为两种形式:
(1)命名导出(Named Exports)
适用于导出多个值,导入时需使用相同名称(可解构或重命名)。
示例:
javascript
// mathUtils.js - 命名导出多个工具函数
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const PI = 3.14159;
// 或集中导出(需提前定义变量)
const subtract = (a, b) => a - b;
export { subtract }; // 直接导出已定义的变量
(2)默认导出(Default Export)
每个模块最多一个默认导出,通常用于导出模块的"主功能"(如组件、类实例)。导入时可自定义名称。
示例:
javascript
// app.js - 默认导出一个对象
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
export default config; // 默认导出
// 或导出匿名函数
export default () => console.log('Hello Module!');
- 导入(Import)
根据导出类型选择不同的导入方式:
(1)导入命名导出
javascript
// 精确导入指定成员(必须与导出名称一致)
import { add, multiply } from './mathUtils.js';
console.log(add(2, 3)); // 5
// 重命名导入(解决名称冲突)
import { PI as圆周率 } from './mathUtils.js';
console.log(圆周率); // 3.14159
// 整体导入(作为命名空间对象)
import * as MathUtils from './mathUtils.js';
console.log(MathUtils.multiply(2, 3)); // 6
(2)导入默认导出
javascript
// 导入默认导出(名称可自定义)
import appConfig from './app.js';
console.log(appConfig.apiUrl); // 'https://api.example.com'
// 同时导入默认导出+命名导出
import React, { useState } from 'react'; // React是默认导出,useState是命名导出
- 动态导入(Dynamic Import)
通过import()函数异步加载模块(返回Promise),适用于按需加载(如路由懒加载、条件加载)。
示例:
javascript
// 按钮点击时加载大模块
document.getElementById('loadChart').addEventListener('click', async () => {
const chartModule = await import('./chartUtils.js');
chartModule.renderChart(); // 调用加载后的模块方法
});
// 路由懒加载(Vue/React常见场景)
const HomePage = () => import('./pages/Home.vue');
2. Vue设计哲学与核心理念
1 渐进式框架
-
概念
Vue的核心特性是其"渐进式"设计. 这意味着它不像某些"大而全"的框架,强迫你全盘接受其所有技术栈.相反.你可以像搭积木一样,根据项目需求,逐步,按需地引入Vue的功能.
- 核心仅关注视图层: Vue的核心库非常小,只专注于将数据渲染到视图. 这使得它容易上手,且能青山集成到任何现有项目中.
- 按需扩展: 当应用变得复杂时,你可以无缝地引入官方提供的解决方案,如Vue Router用于路由管理, Pinia 用于状态管理.
- 场景灵活: 既可以像jQuery一样,通过简单的< script>标签引入,在单个页面上实现交互效果,也可以通过 Vite 或 Vue Cli 构建功能完备,高度工程化的单页应用(SPA).
问:谈谈你对 Vue 渐进式框架的理解?
回答要点:
- 核心定义:首先清晰说明"渐进式"就是"按需取用,逐步增强"。
- 层次说明:分层次阐述其含义。
- 视图层核心:可以只用它做 DOM 渲染,像 jQuery 一样。
- 组件系统:可以只用它来构建可复用的组件。
- 客户端路由:需要页面跳转时,加入 Vue Router。
- 状态管理:应用复杂、组件间通信频繁时,加入 Pinia。
- 构建工具链:对于大型项目,使用 Vite 获得完整的工程化支持。
- 举例说明:结合实际场景,例如"我可以在一个旧的 JSP 或 PHP 页面里,> 只引入 Vue 来做一个交互复杂的表单,而不用重构整个页面"。
- 总结优势:最后总结其优点------灵活、低门槛、风险小,无论是小项目还是大应用都能很好地适应。
2 响应式数据驱动
- 概念
数据驱动视图是Vue的核心灵魂. 它指的是: 我们作为开发者,只需要关系数据的变更,而将频繁,易错的DOM操作完全交给框架去处理 .
当应用的状态(数据)发送变化时,Vue的响应式系统会自动侦测到这些变化,并高效地更新视图中依赖该数据的部分. 这个过程是自动的,声明式的.
问:Vue 的响应式原理是什么?或者说,为什么我修改了数据,页面就自动更新了?
回答要点:
- 核心思想:首先点明是"数据驱动视图",开发者只需关心数据。
- 关键技术 (Vue 3):明确指出 Vue 3 基于 ES6 Proxy 实现。当一个对象被 reactive() 或 ref() 包裹后,它就成了一个代理对象。
- 两个核心阶段:
- 依赖收集:在组件渲染(render)时,模板中用到的响应式数据会被"get",触发 Proxy 的 getter。此时,Vue 会将当前的渲染副作用函数(effect)作为订阅者,与该数据进行关联。
- 派发更新:当修改数据时,会触发 Proxy 的 setter。Vue 会找到之前所有订阅(依赖)了这个数据的副作用函数,并重新执行它们,从而触发组件的重新渲染。
- 优化机制:可以补充提及 Vue 通过 虚拟 DOM 和 Diff 算法 来计算最小更新范围,保证了更新的高性能,而不是暴力地重绘整个页面。
- (加分项) 对比 Vue 2:如果了解,可以补充说明 Vue 2 是通过 Object.defineProperty 实现的,并指出其无法监听数组索引和对象新增属性的缺陷,从而突显 Proxy 的优势。
3 模板友好
- 概念
Vue 选择了基于 HTML 的模板语法。这意味着你可以使用我们已经非常熟悉的 HTML 标签,并通过 Vue 提供的特殊属性(指令,如 v-if, v-for)和插值({{ }})来声明式地描述 UI。
这种设计使得模板非常直观、易于理解,尤其对于有 HTML/CSS 背景的开发者或设计师来说,学习成本极低。
问:"为什么 Vue 默认使用模板语法,而不是像 React 那样使用 JSX?模板语法有什么好处?"
回答要点:
- 降低门槛,提升体验:核心原因是开发体验和学习曲线。模板语法基于标准 HTML,对初学者和设计师非常友好,符合直觉。
- 声明式与直观性:模板能非常清晰、直观地表达 UI 的结构和逻辑,代码可读性强。
- 编译时优化:这是关键的技术优势。强调 Vue 的模板不是直接在运行时解释的,而是被编译器转换成了优化的渲染函数。可以具体说出几点优化,如静态提升、补丁标记等,证明你理解其底层原理。
- 关注点分离:提及单文件组件(SFC)的结构优势,模板、逻辑、样式各司其职,便于团队协作和长期维护。
- 灵活性:最后可以补充一点,Vue 并不排斥 JSX。对于需要高度动态和编程灵活性的场景,开发者完全可以选择使用 JSX,体现了 Vue 设计的灵活性。
3. Composition API与响应式系统
Composition API 是 Vue 3 引入的全新组件逻辑组织方式,基于响应式系统实现状态管理与视图更新。以下从核心概念、技术原理及实践价值等方面展开分析:
一、Composition API 的核心特性
- 灵活的逻辑组织
- 通过 setup 函数将相关逻辑集中管理,替代 Options API 分散的选项(如 data、methods)。例如,一个复杂组件的多个功能模块可拆分为独立的组合函数(Composables),提升代码可读性。
- 优势:解决 mixins 的命名冲突、数据来源不清晰等问题,支持逻辑的高度复用。
- 响应式工具函数
- ref:用于基本数据类型(如数字、字符串),通过 .value 访问值。例如:
javascript
const count = ref(0); // 初始值为0
count.value++; // 修改触发响应式更新
```[^4^][^7^]
-
- reactive:处理对象/数组,自动追踪属性变化。底层基于 ES6 Proxy 实现,无需手动遍历属性。
-
- computed:定义依赖其他响应式数据的计算属性,缓存结果以优化性能。
-
生命周期钩子
- 提供如 onMounted、onUnmounted 等函数式钩子,允许在特定阶段注入逻辑。例如:
javascript
onMounted(() => { console.log('组件已挂载'); });
```[^4^][^7^]
二、响应式系统的底层实现
- 依赖追踪机制
- track:在属性读取时收集依赖(Effect),建立属性与副作用函数的映射。
- trigger:当属性被修改时,触发关联的 Effect 重新执行,驱动视图更新。
- Vue 3 响应式系统的改进
- 代理拦截:使用 Proxy 替代 Object.defineProperty,支持动态属性添加、数组原生方法拦截等。
- 惰性代理:仅在首次访问属性时创建依赖,减少初始化开销。
三、应用场景与最佳实践
- 逻辑复用与模块化
- 通过自定义组合函数封装有状态逻辑。例如,封装 useMouse 跟踪鼠标位置,可在多组件中复用。
- 嵌套组合:支持组合函数之间的调用,形成复杂逻辑链。
- 性能优化策略
- 避免冗余更新:合理使用 shallowRef 或 readonly 限制深层响应式追踪。
- 依赖清理:利用 watchEffect 的停止函数处理副作用,防止内存泄漏。
四、总结
- 开发效率:通过函数式组合简化复杂逻辑的管理,增强代码可维护性。
- 性能表现:基于 Proxy 的响应式系统减少了不必要的更新,提升渲染效率。
- 生态适配:与 TypeScript 深度集成,支持静态类型检查,降低大型项目的开发风险。
总之,Composition API 不仅是语法层面的升级,更是 Vue 响应式系统与现代前端工程化理念的结合。通过深入理解其设计思想与实现原理,开发者可构建高内聚、低耦合的高质量应用。
问:"为什么 Vue 3 要引入 Composition API?它解决了什么问题?"
回答要点:
- 先肯定 Options API:"Options API 在中小型项目中非常直观易懂,这是 Vue 易学性的重要体现。"
- 再指出其痛点:"然而,在开发大型复杂组件时,Options API 会导致逻辑关注点分散,相关代码被拆分到 data, methods 等不同选项中,难以维护。同时,依赖 mixins 的逻辑复用方案存在命名冲突和数据来源不清晰等问题,且对 TypeScript 的类型推断不够友好。"
- 最后引出解决方案:"Composition API 正是为了解决这些问题而生的。它允许我们通过组合函数 的方式,将相关逻辑聚合在一起,实现了更清晰、更可维护、类型更安全的代码组织和逻辑复用模式。"
- 补充说明:"需要强调的是,Composition API 是对 Options API 的补充而非替代,开发者可以根据项目复杂度和团队习惯灵活选择。"
4. v-if 和 v-show 有什么区别,应该如何选择?
- 区别:
- 原理: v-if 是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件被适当地销毁和重建。v-show 只是简单地切换元素的 CSS display 属性。
- 性能: v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
- 选择:
- 如果需要频繁地切换,使用 v-show 性能更好。
- 如果条件在运行时很少改变,或者初始为假时不需要渲染任何东西,使用 v-if 更合适。
5. 在 Vue3 中, v-if 和 v-for 一起使用时,哪个优先级更高?
- 在 Vue 3 中,v-if 的优先级高于 v-for。这意味着 v-if 会先生效,此时它还无法访问到 v-for 作用域中的变量。
- 最佳实践是避免将它们放在同一个元素上。推荐的做法是:
- 使用一个计算属性(computed)来预先过滤掉不需要显示的项,然后在过滤后的列表上使用 v-for。
- 将 v-for 移到 标签上,然后在内部的元素上使用 v-if。
html
<!-- 推荐:使用计算属性 -->
<div v-for="item in visibleItems" :key="item.id">{{ item.name }}</div>
<!-- 备选:使用 <template> 标签 -->
<template v-for="item in items" :key="item.id">
<div v-if="item.isVisible">{{ item.name }}</div>
</template>
const filteredUsers = computed(() => users.value.filter(u => u.age > 26));
6. 除了 props/emit,你还知道哪些组件通信方式?
- v-model: 用于在父子组件间创建双向绑定,非常适合封装表单控件。
- provide/inject: 用于跨越多层的祖先到后代的通信,可以有效解决"prop drilling"问题,常用于传递全局配置或主题信息。
- 状态管理库 (Pinia/Vuex): 当多个组件需要共享和操作同一份复杂状态时,应使用集中的状态管理方案,以保证数据流的可预测性和可维护性。
✅最佳实践
- 数据单向流动: 始终坚持父组件通过 props 向下传递数据,子组件通过 emit 事件通知父组件进行状态变更。子组件永远不应直接修改 props。
- key 的重要性: 在使用 v-for 时,总是提供一个唯一的、稳定的 key 值(如 item.id),而不是使用 index,这对于性能优化和避免状态混淆至关重要。
- 选择合适的通信方式:
- 父子: 默认使用 props/emit。
- 深层嵌套: 考虑 provide/inject。
- 兄弟或远亲: 提升状态到共同的父组件,或使用 Pinia。
- 组件接口清晰: 为 props 提供明确的类型、默认值和校验规则。为 emits 做出明确的声明。这使得组件像一份清晰的 API 文档,易于理解和使用。
7. 模板语法深入解析(v-bind / v-model / v-for)
Vue 的指令(Directives)是带有 v- 前缀的特殊属性,用于在渲染的 DOM 上应用响应式行为。
7.1 v-bind (缩写 : ) - 动态属性绑定
用于动态地绑定一个或多个 HTML 属性。
<script setup>
import { ref, reactive } from 'vue';
const imageUrl = ref('logo.png');
const imageAlt = ref('Company Logo');
const isDisabled = ref(true);
// 使用对象一次性绑定多个属性
const elementAttributes = reactive({
id: 'unique-element',
'data-index': 123
});
</script>
<template>
<!-- 基础用法 -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- 绑定布尔值属性 -->
<button :disabled="isDisabled">Click Me</button>
<!-- 绑定一个包含多个属性的对象 -->
<div v-bind="elementAttributes"></div>
</template>
7.2 v-model - 双向数据绑定
v-model 是一个语法糖,用于在表单输入元素或自定义组件上创建双向数据绑定。它会根据元素类型自动选择正确的方式来更新值。
核心原理: :value + @input 事件的结合。
修饰符:
-
.lazy: 将 input 事件同步改为 change 事件同步。
-
.number: 将输入值自动转为数字。
-
.trim: 自动过滤用户输入的首尾空白字符。
<script setup> import { ref } from 'vue';const message = ref('');
<template>
const age = ref(0);
const username = ref('');
</script>Message is: {{ message }}
</template><!-- .number 修饰符 --> <input v-model.number="age" type="number" /> <!-- .trim 修饰符 --> <input v-model.trim="username" />
7.3 v-for - 列表渲染
用于基于一个数组来渲染一个列表。
-
关键: 必须使用 key 属性为每个节点提供一个唯一的标识,以帮助 Vue 高效地更新 DOM。
-
适用范围: 可以遍历数组、对象、数字和字符串。
<script setup> import { ref } from 'vue';const items = ref([
<template>
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
]);
</script>- {{ index }} - {{ item.name }}
8. 组件通信模式详解(props / emit / slots)
父子通信:props / emit / slots
这是最常用和最基础的通信方式。
- props (父 -> 子): 父组件通过属性将数据向下传递给子组件。
- emit (子 -> 父): 子组件通过触发事件将信息发送给父组件。
- slots (父 -> 子内容分发): 父组件可以将模板片段(内容)插入到子组件指定的位置。
案例一: props 父--> 子
父组件 (Parent.vue)
javascript
<template>
<div>
<h2>父组件</h2>
<!-- 传递不同类型的 prop -->
<ChildComponent
:message="parentMsg" <!-- 字符串 -->
:count="itemCount" <!-- 数字 -->
:isVisible="true" <!-- 布尔值 -->
:userInfo="userData" <!-- 对象 -->
:tags="['Vue', 'React']" <!-- 数组 -->
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMsg = ref('Hello from Parent');
const itemCount = ref(10);
const userData = reactive({ name: 'Alice', age: 25 });
</script>
子组件 (ChildComponent.vue)
javascript
<template>
<div class="child">
<p>收到的消息:{{ message }}</p>
<p>数量:{{ count }}</p>
<p>是否可见:{{ isVisible ? '是' : '否' }}</p>
<p>用户信息:{{ userInfo.name }} - {{ userInfo.age }}岁</p>
<p>标签列表:<span v-for="tag in tags" :key="tag">{{ tag }}</span></p>
</div>
</template>
<script setup>
// 声明接收的 props(自动继承自父组件)
defineProps({
// 字符串类型
message: {
type: String,
required: true, // 必须传递
default: '' // 默认值(当父组件未传递时生效)
},
// 数字类型 + 默认值
count: {
type: Number,
default: 0
},
// 布尔值
isVisible: {
type: Boolean,
default: false
},
// 对象类型 + 校验函数
userInfo: {
type: Object,
default: () => ({ name: 'Guest', age: 0 }), // 工厂函数返回新对象
validator: (value) => { // 自定义校验规则
return ['name', 'age'].every(key => key in value);
}
},
// 数组类型
tags: {
type: Array,
default: () => [] // 默认空数组
}
});
</script>
案例二: emit (子 -> 父)
本质是触发自定义事件,父组件通过监听事件接收子组件传递的参数
1. 父组件(接收事件和数据)
javascript
<!-- Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<!-- 监听子组件的自定义事件(用 @ 语法) -->
<ChildComponent
@send-msg="handleReceiveMsg"
@update-count="handleUpdateCount"
/>
<p>收到的消息:{{ receivedMsg }}</p>
<p>收到的数量:{{ receivedCount }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const receivedMsg = ref('');
const receivedCount = ref(0);
// 父组件监听子组件的自定义事件,接收子组件传递的参数
const handleReceiveMsg = (msg) => {
receivedMsg.value = msg;
};
const handleUpdateCount = (count) => {
receivedCount.value = count;
};
</script>
- 子组件(触发事件并传递数据)
javascript
<!-- ChildComponent.vue -->
<template>
<div class="child">
<button @click="sendMsgToParent">向父组件发送消息</button>
<button @click="updateCount">向父组件更新数量</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
// 定义子组件可以触发的自定义事件(事件名需与父组件监听名一致)
const emit = defineEmits(['send-msg', 'update-count']);
// 触发事件并传递参数
const sendMsgToParent = () => {
const msg = 'Hello from Child!';
emit('send-msg', msg); // 第一个参数是事件名,后续是传递的数据
};
const updateCount = () => {
const count = Math.floor(Math.random() * 100);
emit('update-count', count);
};
</script>
双向绑定:在自定义组件上使用 v-model
你可以在自己的组件上实现 v-model,这对于创建自定义表单控件尤其有用。
- 约定: 组件接收一个名为 modelValue 的 prop,并通过 update:modelValue 事件来更新它。
父组件 ParentForm.vue
javascript
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const searchText = ref('Initial Text'); // 响应式数据
</script>
<template>
<CustomInput v-model="searchText" /> <!-- 关键:使用 v-model 绑定 -->
<p>Current search text: {{ searchText }}</p>
</template>
子组件 CustomInput.vue
javascript
<template>
<!-- 核心:用 :value 接收 prop,@input 触发更新事件 -->
<input
type="text"
:value="modelValue" <!-- 接收父组件传递的值 -->
@input="$emit('update:modelValue', $event.target.value)" <!-- 发出更新事件 -->
/>
</template>
<script setup>
// 声明接收 prop(必须命名为 modelValue)
defineProps(['modelValue']);
// 声明可触发的事件(必须命名为 update:modelValue)
defineEmits(['update:modelValue']);
</script>
针对多个属性双向绑定
javascript
<!-- 父组件 -->
<CustomInput
v-model:title="title"
v-model:content="content"
/>
<!-- 子组件 -->
<script setup>
defineProps({
title: String, // 对应 v-model:title
content: String // 对应 v-model:content
});
defineEmits(['update:title', 'update:content']);
</script>
跨层级通信:provide / inject
当需要从祖先组件向其所有后代组件传递数据时,使用 provide 和 inject 可以避免逐层传递 props(即"prop drilling")。
- provide: 在祖先组件中提供数据或方法。
- inject: 在任何后代组件中注入(接收)这些数据或方法。
祖先组件 App.vue
javascript
<script setup>
import { ref, provide } from 'vue';
import DeepChild from './DeepChild.vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
// 提供数据和方法给所有后代
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<template>
<div :class="theme">
<DeepChild />
</div>
</template>
后代组件 DeepChild.vue
javascript
<script setup>
import { inject } from 'vue';
// 注入来自祖先的数据和方法
const theme = inject('theme', 'light'); // 'light' 是默认值
const toggleTheme = inject('toggleTheme');
</script>
<template>
<p>Current theme is: {{ theme }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</template>
9. Vue 3 的生命周期相比 Vue 2 有什么核心变化?
回答要点:
- 从选项到函数:最大的变化是在 Composition API 中,生命周期从 Options API 的对象属性变成了需要导入的函数,如 onMounted。
- setup 的整合:setup 函数在时机上替代了 beforeCreate 和 created,成为组件初始化的入口。
- 命名变更:beforeDestroy 和 destroyed 被更名为了 onBeforeUnmount 和 onUnmounted,语义更清晰。
- 优势:这种函数式的转变让逻辑组织更灵活,可以轻松地将相关联的副作用逻辑(如创建和清理)聚合在一起,并通过组合式函数(Composables)实现复用。
10. watch 和 watchEffect 有什么区别?应该如何选择?
回答要点:
- 依赖追踪:
- watchEffect:自动追踪。它会自动收集其回调函数中访问到的所有响应式数据作为依赖。
- watch:手动指定。你必须明确地告诉它要监听哪个数据源。
- 执行时机:
- watchEffect:立即执行一次,然后等待依赖变化后再次执行。
- watch:默认是懒执行的,只有当被监听的数据源变化时才执行。可以通过 { immediate: true } 选项使其立即执行。
- 访问旧值:
- watchEffect:无法访问变化前的值。
- watch:可以同时访问新值和旧值,方便进行比较。
如何选择:
- 用 watch:当你想精确控制监听目标,或者当副作用逻辑需要依赖旧值时。这是更常见的选择。
- 用 watchEffect:当副作用的依赖项很多,或者依赖关系不那么直观时,让 Vue 自动追踪会更方便。
11. Vue 3 生命周期:新变化与核心理念
一 核心变化
Vue 3 对生命周期系统进行了重要调整,核心变化体现在 Composition API 中:
- 全面函数化:所有生命周期钩子都变成了可以在 setup() 中调用的函数 (例如 onMounted, onUpdated)。这使得相关逻辑可以轻松地抽离到独立的组合函数中,极大提高了复用性。
- 命名统一:废弃了 beforeDestroy 和 destroyed,统一为 onBeforeUnmount 和 onUnmounted,更准确地描述了组件"卸载"的过程。
- setup 的中心地位:setup 函数在组件实例创建之前执行,它在时机上包揽了 Vue 2 中的 beforeCreate 和 created,成为组件初始化的核心入口。
- 新增调试钩子:增加了 onRenderTracked 和 onRenderTriggered,用于在开发模式下追踪和调试组件的响应式依赖。
二 代码对比:Options API vs Composition API
Options API (Vue 2/3)
javascript
// 逻辑分散在不同的选项中
export default {
data() {
return {
message: 'Hello',
timer: null
}
},
mounted() {
console.log('DOM 挂载完成');
this.timer = setInterval(() => {
console.log('定时任务');
}, 1000);
},
beforeUnmount() { // Vue 2 中是 beforeDestroy
console.log('组件即将卸载');
if (this.timer) {
clearInterval(this.timer);
}
}
}
Composition API (Vue 3)
javascript
import { ref, onMounted, onBeforeUnmount } from 'vue';
// 所有相关逻辑都聚合在 setup 中
export default {
setup() {
const message = ref('Hello');
let timer = null;
// 生命周期钩子作为函数调用
onMounted(() => {
console.log('DOM 挂载完成');
timer = setInterval(() => {
console.log('定时任务');
}, 1000);
});
// 清理逻辑也在这里,代码关联性更强
onBeforeUnmount(() => {
console.log('组件即将卸载');
if (timer) {
clearInterval(timer);
}
});
return { message };
}
}
三 生命周期钩子映射表

12 Vue3 副作用(Side Effects)管理
副作用是指组件中会影响外部环境的操作,如网络请求、DOM 操作、定时器和事件监听等。妥善管理副作用是保证应用稳定性的关键。
一 使用生命周期钩子管理
最基础的副作用管理方式就是利用生命周期钩子,在合适的时机创建和销毁副作用。
- onMounted:组件挂载到 DOM 后执行,适合执行需要访问 DOM 的操作或一次性的初始化任务。
- onBeforeUnmount / onUnmounted:组件卸载前/后执行,是清理副作用(如定时器、全局事件监听)的理想位置,防止内存泄漏。
javascript
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
let timer = null;
const handleResize = () => { /* ... */ };
// 在 onMounted 中设置副作用
onMounted(() => {
const element = document.querySelector('#my-element');
if (element) element.focus(); // DOM 操作
timer = setInterval(() => console.log('tick'), 1000); // 定时器
window.addEventListener('resize', handleResize); // 事件监听
});
// 在 onBeforeUnmount 中清理副作用
onBeforeUnmount(() => {
clearInterval(timer);
window.removeEventListener('resize', handleResize);
});
return {};
}
}
- onUpdated:在组件因响应式数据变化而更新 DOM 后调用。适用于需要操作更新后 DOM 的场景,例如聊天窗口滚动到底部。
javascript
import { ref, onUpdated, nextTick } from 'vue';
export default {
setup() {
const list = ref([]);
const scrollContainer = ref(null); // <div ref="scrollContainer"></div>
onUpdated(() => {
// DOM 更新后,将滚动条滚动到底部
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight;
}
});
// 注意:onUpdated 在每次更新后都会执行。
// 如果想基于特定数据的变化来执行操作,`watch` 是更好的选择。
return { list, scrollContainer };
}
}
二 使用 Watchers 精确管理
对于需要响应特定数据变化的副作用,Vue 提供了 watch 和 watchEffect,它们提供了比生命周期钩子更精确的控制力。
watchEffect: 自动追踪依赖
watchEffect 会立即执行一次,然后自动追踪其回调函数中所有使用到的响应式依赖。当任何一个依赖变化时,它会重新运行。
- 优点:简单直接,无需手动指定依赖。
- 适用场景:当副作用的依赖关系复杂或不确定时。
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const userId = ref('1');
const userData = ref(null);
// watchEffect 会自动追踪 userId.value 的变化
watchEffect(async (onInvalidate) => {
if (!userId.value) return;
const controller = new AbortController();
// onInvalidate 注册一个清理函数,在副作用重新执行或组件卸载前调用
onInvalidate(() => controller.abort());
try {
userData.value = await fetch(`/api/users/${userId.value}`, { signal: controller.signal });
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Failed to fetch user data:', error);
}
}
});
return { userId, userData };
}
}
watch: 明确指定依赖
watch 让你明确指定要监听的一个或多个数据源,并在它们变化时执行回调。
- 优点:控制更精确,可以访问新值和旧值,并且可以通过选项(deep, immediate)进行深度监听或立即执行。
- 适用场景:当你想在特定数据变化时执行逻辑,或者需要旧数据进行比较时。
javascript
import { ref, watch } from 'vue';
export default {
setup() {
const keyword = ref('');
const results = ref([]);
// 1. 监听单个 ref
watch(keyword, async (newVal, oldVal) => {
if (newVal.length > 1) {
results.value = await fetch(`/api/search?q=${newVal}`);
} else {
results.value = [];
}
});
const filters = ref({ price: 100, category: 'books' });
// 2. 深度监听对象
watch(
filters,
(newFilters) => {
// 当 filters 内部属性变化时执行
console.log('Filters changed:', newFilters);
},
{ deep: true } // 必须开启 deep 选项
);
return { keyword, results, filters };
}
}
三. 与 React useEffect 对比
Vue 的 watch/watchEffect 与 React 的 useEffect 目的相似,但心智模型不同。

代码对比:实现相同功能
Vue (watchEffect)
javascript
// Vue: 自动追踪 userId 的变化
import { ref, watchEffect } from 'vue';
const userId = ref(1);
const user = ref(null);
watchEffect(async (onInvalidate) => {
const controller = new AbortController();
onInvalidate(() => controller.abort());
user.value = await fetchUser(userId.value, controller.signal);
});
React (useEffect)
javascript
// React: 必须在依赖数组中手动指定 userId
import { useState, useEffect } from 'react';
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, controller.signal).then(setUser);
return () => controller.abort(); // 返回清理函数
}, [userId]); // 手动指定依赖数组
四. 最佳实践与常见陷阱
✅ 最佳实践
- 逻辑聚合:将创建副作用(如 setInterval)和清理它的逻辑(clearInterval)放在一起,最好使用 onMounted 和 onBeforeUnmount 配对。
- 条件性执行:在 watch 或 watchEffect 内部添加条件判断,避免在不必要时(如搜索词太短)执行昂贵的操作。
- 优先选择 watch:当你明确知道副作用依赖哪个状态时,优先使用 watch,因为它的意图更清晰。当依赖关系复杂或不确定时,再考虑 watchEffect。
❌ 常见陷阱
- 忘记清理:最常见的错误是在组件卸载时忘记清理定时器、事件监听器或 WebSocket 连接,这会导致严重的内存泄漏。
javascript
// ❌ 错误:忘记在 onBeforeUnmount 中清理
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
// ✅ 正确
onMounted(() => window.addEventListener('scroll', handleScroll));
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll));
- 在错误的生命周期访问 DOM:在 setup 中直接访问 DOM 会失败,因为此时组件尚未挂载。所有 DOM 操作都应在 onMounted 之后进行。
javascript
// ❌ 错误:setup 执行时 DOM 不存在
setup() {
const el = document.getElementById('my-element'); // el is null
}
// ✅ 正确
setup() {
onMounted(() => {
const el = document.getElementById('my-element'); // el is available
});
}
13. 陷阱 1:解构 reactive 对象导致失去响应性
直接解构 reactive 对象会使其属性变为普通变量,从而失去响应性。
javascript
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({ count: 0, name: 'John' })
// ❌ 错误:count 和 name 只是普通变量,不再具有响应性
// 当 state.count 改变时,这里的 count 不会更新
const { count, name } = state
// ✅ 正确:使用 toRefs()
// toRefs 会将 reactive 对象的每个属性都转换为一个 ref
const stateAsRefs = toRefs(state)
// 现在 stateAsRefs.count 和 stateAsRefs.name 都是 ref,可以在模板中安全使用
return {
// 如果直接返回 count, 它不是响应式的
// 必须返回 toRefs 转换后的结果
...stateAsRefs // 使用扩展运算符将 { count: ref, name: ref } 返回
}
}
}
14. 陷阱 2:错误地替换响应式对象
对于 ref 包裹的对象或数组,修改时必须通过 .value 属性进行。
javascript
import { ref } from 'vue'
export default {
setup() {
let list = ref([1, 2, 3])
const updateList = () => {
// ❌ 错误:这会使 list 变量指向一个新的普通数组,原来的响应式引用丢失了
// list = [4, 5, 6]
// ✅ 正确:始终通过 .value 来修改 ref 的值
list.value = [4, 5, 6]
}
return { list, updateList }
}
}
15. Vue Router与Pinia状态管理
Vue Router 核心概念与应用
Vue Router 是 Vue.js 的官方路由管理器。它能帮助我们构建单页面应用(SPA),将组件映射到不同的 URL 路径。
首先,我们定义一个路由表 routes,它是一个由多个路由配置对象组成的数组,每个对象描述一个页面路径与对应组件的映射关系。
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
// 1. 定义路由表
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
// 2. 创建路由实例
const router = createRouter({
// 3. 配置路由模式
history: createWebHistory(), // 使用 HTML5 History 模式
routes // 等价于 routes: routes
});
export default router;
这里有几个关键点值得注意:
-
history: createWebHistory()
表示使用 HTML5 的 History 模式,URL 是普通路径形式,例如(https://example.com/about), 没有 #
这种模式的好处是更美观、更贴近真实网站结构,但需要服务器做一些额外配置------无论访问哪个路径,都应该返回 index.html,否则刷新页面会 404。
-
另一种方式:createWebHashHistory()
如果不想配置服务器,可以使用 hash 模式,路径中会带 #,例如:https://example.com/#/about。这种方式兼容性更好,但 URL 略显"丑陋"。
在 Vue 组件中使用
使用 生成导航链接,使用 渲染当前路由匹配的组件。
javascript
<!-- App.vue -->
<template>
<header>
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</nav>
</header>
<!-- 路由出口:当前路由匹配的组件将在这里渲染 -->
<main>
<router-view />
</main>
</template>
2. 高级路由特性
动态路由匹配
当需要将具有给定模式的路由映射到同一个组件时,可以使用动态路由。例如,一个 User 组件需要根据不同的用户 ID 显示不同内容。
javascript
// router/index.js
import User from '@/views/User.vue';
const routes = [
// `:id` 是一个动态段,可以匹配任意字符串
{
path: '/user/:id',
name: 'User',
component: User,
props: true // 将路由参数(如 id)作为 props 传递给组件
}
];
在 User.vue 组件中,可以通过 props 接收 id:
javascript
<script setup>
// 在 <script setup> 中,通过 defineProps 接收
const props = defineProps(['id']);
console.log('当前用户ID:', props.id);
</script>
嵌套路由
对于复杂的布局,可以使用嵌套路由。父路由拥有自己的 来渲染子路由组件。
javascript
// router/index.js
// ... 假设已导入 User, UserProfile, UserSettings 组件
{
path: '/user/:id',
name: 'User',
component: User, // User.vue 是父路由组件
props: true,
children: [ // 子路由
{
// 当 URL 是 /user/:id 时,UserProfile 会在 User 的 <router-view> 中渲染
path: '',
component: UserProfile
},
{
// 当 URL 是 /user/:id/settings 时,UserSettings 会在 User 的 <router-view> 中渲染
path: 'settings',
component: UserSettings
}
]
}
User.vue 组件需要包含 来显示子组件:
javascript
<!-- User.vue -->
<template>
<div>
<h1>用户 {{ id }} 的主页</h1>
<!-- 子路由组件的渲染出口 -->
<router-view />
</div>
</template>
路由懒加载
当应用规模变大时,可以将不同路由的组件分割成不同的代码块(chunk),然后在访问路由时才加载它们。这可以显著提升首屏加载速度。
javascript
// router/index.js
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
// 只有在访问 /dashboard 时,才会下载并执行 Dashboard.vue 的代码
component: () => import('@/views/Dashboard.vue')
}
];
重定向与别名
- 重定向: 当用户访问 /home 时,URL 会被替换成 /,然后匹配 / 的路由。
- 别名: 访问 /people 的效果与访问 /users 完全一样,但 URL 保持为 /people。
javascript
// ... 假设已导入 UserList 组件
const routes = [
// 重定向
{ path: '/home', redirect: '/' },
// 别名
{ path: '/users', component: UserList, alias: '/people' }
];
3. 编程式导航
除了使用 ,我们还可以在 JavaScript 代码中控制路由跳转。
javascript
// 在组件的 <script setup> 中
import { useRouter } from 'vue-router';
const router = useRouter();
// 跳转到指定路径
const goToDashboard = () => {
router.push('/dashboard');
};
// 带参数跳转
const goToUser = (userId) => {
router.push({ name: 'User', params: { id: userId } });
};
// 替换当前历史记录,用户无法通过后退按钮返回
const replaceToLogin = () => {
router.replace('/login');
};
// 前进或后退
const goBack = () => {
router.go(-1); // 或 router.back()
};
4. 路由守卫
路由守卫(Navigation Guards)提供了在路由跳转过程中执行逻辑的机会,常用于权限验证、数据预取等场景。
全局前置守卫 (beforeEach)
beforeEach 是最常用的守卫,它在任何路由跳转发生之前被调用。
javascript
// router/index.js
// import { useUserStore } from '@/stores/user' // 示例:从 Pinia store 获取用户状态
router.beforeEach((to, from, next) => {
const isAuthenticated = false; // 示例:实际应从 store 或 cookie 获取
// 检查路由是否需要认证
if (to.meta.requiresAuth && !isAuthenticated) {
// 用户未登录,重定向到登录页
next({ name: 'Login', query: { redirect: to.fullPath } });
} else {
// 允许导航
next();
}
});
- to: 即将进入的目标路由对象。
- from: 当前导航正要离开的路由对象。
- next: 必须调用的函数,以解析这个钩子。
- next(): 继续导航。
- next(false): 中断当前导航。
- next('/path') 或 next({ name: '...' }): 重定向到新的地址。
Pinia 状态管理深入
Pinia 是 Vue 官方推荐的状态管理库。它以更简洁的 API、出色的 TypeScript 支持和对 Composition API 的友好性,成为了 Vuex 的现代替代品。
- Store 定义与使用
一个 Store(仓库)是使用 defineStore 定义的,它包含三部分核心内容:state、getters 和 actions。
- State: 响应式的数据源,类似于组件的 data。
- Getters: 计算属性,类似于组件的 computed,用于派生 state。
- Actions: 方法,类似于组件的 methods,用于修改 state,可以包含异步操作。
基础 Store 示例 (stores/user.js)
javascript
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// --- State ---
const user = ref(null);
const token = ref('');
// --- Getters ---
const isAuthenticated = computed(() => !!user.value && !!token.value);
const userName = computed(() => user.value?.name || '游客');
// --- Actions ---
async function login(credentials) {
// 假设 authAPI 是一个处理网络请求的模块
// const response = await authAPI.login(credentials);
// user.value = response.user;
// token.value = response.token;
// 模拟登录成功
user.value = { name: '张三', email: 'zhangsan@example.com' };
token.value = 'fake-jwt-token-string';
}
function logout() {
user.value = null;
token.value = '';
}
return {
// State
user,
token,
// Getters
isAuthenticated,
userName,
// Actions
login,
logout
};
});
注意:在 Composition API 风格的 store 中,ref() 定义 state,computed() 定义 getters,普通 function 定义 actions。
在组件中使用 Store
javascript
// stores/cart.js
import { defineStore } from 'pinia';
import { useUserStore } from './user'; // 导入用户 store
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore(); // 在另一个 store 中获取实例
async function checkout() {
if (userStore.isAuthenticated) {
console.log(`用户 ${userStore.userName} 正在结算...`);
// 执行结算逻辑...
} else {
console.log('请先登录再结算。');
}
}
return { checkout };
});
16. Pinia 相比 Vuex 有什么优势?
答题思路:从设计理念、API 简洁性和开发体验三个方面进行对比,并用表格清晰展示。

总结:Pinia 的主要优势在于其简洁直观的 API、移除了 Mutations的心智负担、出色的 TypeScript 支持以及更自然的模块化方式。它让状态管理代码更易于编写和维护。
17. 如何设计一个大型应用的路由结构?
答题思路:从模块化、权限控制、性能和可维护性四个角度阐述。
一个健壮的路由结构应该具备以下特点:
- 模块化:按业务功能或页面区域(如 后台管理、用户中心)将路由配置拆分到不同的文件中,再由主路由文件统一导入整合。这样可以避免单个路由文件过于庞大。
- 权限控制:
- 在路由的 meta 字段中定义权限信息,如 meta: { roles: ['admin'] }。
- 使用全局路由守卫 router.beforeEach 来检查用户角色和 meta 字段,实现页面访问控制。
- 对于需要根据用户权限动态生成的菜单,可以后端返回路由数据,前端进行动态添加 (router.addRoute())。
- 性能优化:
- 全量使用懒加载:对所有页面级组件使用 () => import(...) 进行懒加载,这是最关键的性能优化手段。
- 预加载(Prefetching):对于用户很可能访问的下一个页面,可以考虑使用 webpack 的魔法注释 /* webpackPrefetch: true */ 进行资源预加载。
- 可维护性:
- 统一命名:为路由 name 属性制定清晰、唯一的命名规范,方便编程式导航和缓存控制()。
- 目录结构清晰:将路由配置文件、视图组件、路由相关的工具函数等分门别类存放。
18. 如何处理 Pinia 中的异步操作和错误?
答题思路:展示一个包含 loading 和 error 状态管理的标准异步 action 模式。
在 Pinia 中,异步操作通常在 actions 中使用 async/await 完成。一个健壮的实践是同时管理加载状态(loading)和错误状态(error)。
javascript
// stores/data.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useDataStore = defineStore('data', () => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
async function fetchData() {
loading.value = true;
error.value = null; // 重置之前的错误
try {
// 假设 myApi.get() 是一个返回 Promise 的 API 请求函数
const response = await myApi.get('/some-data');
data.value = response.data;
} catch (e) {
// 捕获错误并存储
error.value = e;
// 可以选择将错误再次抛出,让调用方处理 UI 反馈,如弹窗提示
throw e;
} finally {
// 确保 loading 状态总是被重置
loading.value = false;
}
}
return { data, loading, error, fetchData };
});
最佳实践:
- 分离状态:用 loading 和 error 两个独立的 state 来追踪异步流程。
- UI 绑定:在组件中,可以直接使用 v-if="store.loading" 显示加载指示器,或 v-if="store.error" 显示错误信息。
- 错误抛出:在 catch 块中再次 throw 错误,可以让组件层捕获到具体的失败,从而执行如"消息提示"、"跳转页面"等交互。
- finally 清理:使用 finally 确保无论成功还是失败,loading 状态都会被正确地设置为 false。
19. Vue vs React对比
核心差异总览

1. 核心设计理念对比
Vue:渐进式框架 (Progressive Framework)
"渐进式"意味着你可以根据项目需求,逐步、分层地引入 Vue 的功能。
- 简单场景:可以像引入 jQuery 一样,只使用 Vue 的核心库来做数据绑定和 DOM 更新。
- 复杂场景:可以配合官方的路由(Vue Router)、状态管理(Pinia)和构建工具(Vite),构建功能完备的大型单页应用。
这种灵活性使得 Vue 的学习曲线非常平缓,团队可以根据自身情况选择使用深度。
React:专注于 UI 的库 (A JavaScript library for building user interfaces)
React 的核心只关注 UI 的渲染。它本身不提供路由或全局状态管理方案,而是将这些交由社区生态来解决。
这带来了两个特点:
- 灵活性高:开发者可以自由组合技术栈,例如路由可以用 React Router,状态管理可以用 Redux、Zustand 或 Jotai。
- 门槛较高:要构建一个完整的应用,你从一开始就需要学习 JSX 语法、组件化思想以及如何搭配生态中的其他库。
2. 响应式系统对比
这是两者最根本的技术差异之一。
Vue:透明的、细粒度的响应式
Vue 3 使用 Proxy 实现了自动依赖追踪。当你读取一个响应式数据时,Vue 会"记录"下来;当这个数据变更时,Vue 能精确地知道哪些地方用到了它,并只更新这些地方。
javascript
import { ref, computed } from 'vue';
const count = ref(0); // 声明一个响应式数据
// computed 会自动追踪 count 的变化
const doubled = computed(() => count.value * 2);
// 在模板或 watchEffect 中使用数据,就建立了依赖关系
watchEffect(() => {
console.log(`值变为: ${count.value}`);
});
// 当你修改 count,所有依赖它的地方都会自动更新
count.value++; // 控制台会输出 "值变为: 1"
优势:开发者无需关心依赖管理,心智负担小,且性能优化在很大程度上是自动的。
React:手动的、组件级的响应式
React 的更新基于不可变数据 (Immutability) 和手动依赖声明。
- 你不能直接修改 state,而是通过 setState 提供一个新值。
- React 会在 state 变化时重新渲染整个组件。
- 为了避免不必要的计算和子组件的重渲染,你需要手动使用 useMemo、useCallback 和 memo,并明确提供依赖数组 []。
javascript
import { useState, useMemo, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 必须手动声明依赖 [count],React 才知道何时重新计算
const doubled = useMemo(() => count * 2, [count]);
// 同样需要手动声明依赖,否则可能导致无限循环或不执行
useEffect(() => {
console.log(`值变为: ${count}`);
}, [count]);
// 使用 useCallback 来防止函数在每次渲染时都重新创建
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 空数组表示函数永不改变
return <div onClick={handleClick}>{doubled}</div>;
}
优势:数据流向更明确,对于大型、复杂状态的追踪可能更清晰。
劣势:心智负担重,开发者需要时刻关注依赖数组的正确性,容易因忘记优化而导致性能问题。
等等