vue 面试题
1. vue2有哪些生命周期?和vue3有什么区别?
Details
在 Vue.js 中,生命周期是指 Vue 实例从创建到销毁的各个阶段。在 Vue 2 和 Vue 3 中,生命周期钩子存在一些相似之处,但在 Vue 3 中引入了组合式 API,使得生命周期的使用方式有所不同。以下是 Vue 2 和 Vue 3 的生命周期钩子的详细说明及其区别。
Vue 2 的生命周期钩子
Vue 2 中的生命周期钩子主要包括以下几个:
- beforeCreate:实例初始化之后,数据观测和事件配置之前调用。
- created:实例创建完成后调用,此时数据已被观测,事件已被设置,但尚未挂载。
- beforeMount:在挂载开始之前调用,此时模板已编译,但未被插入 DOM。
- mounted:挂载完成后调用,此时 DOM 已经被插入,适合进行 DOM 操作。
- beforeUpdate:数据更新前调用,可以在这里访问更新前的状态。
- updated:数据更新后调用,此时 DOM 已经重新渲染。
- beforeDestroy:实例销毁前调用,可以在这里做一些清理工作,如取消订阅。
- destroyed:实例销毁后调用,此时所有的事件监听和子实例都已被移除。
Vue 3 的生命周期钩子
Vue 3 的生命周期钩子与 Vue 2 类似,但可以通过组合式 API 来调用。以下是 Vue 3 的生命周期钩子(与 Vue 2 相同):
- beforeCreate:同 Vue 2。
- created:同 Vue 2。
- beforeMount:同 Vue 2。
- mounted:同 Vue 2。
- beforeUpdate:同 Vue 2。
- updated:同 Vue 2。
- beforeUnmount:取代 Vue 2 中的
beforeDestroy,在实例销毁之前调用。 - unmounted:取代 Vue 2 中的
destroyed,在实例销毁后调用。
Vue 2 和 Vue 3 的主要区别
名称变化:Vue 3 中将
beforeDestroy改名为beforeUnmount,将destroyed改名为unmounted,使名称更符合直观操作。组合式 API:在 Vue 3 中,生命周期钩子可以在
setup()函数内使用,您可以通过导入相应的生命周期钩子进行调用。例如:javascriptimport { onMounted, onBeforeUnmount } from 'vue'; export default { setup() { onMounted(() => { console.log('组件已挂载'); }); onBeforeUnmount(() => { console.log('组件即将卸载'); }); } }更好的逻辑复用:通过组合式 API,生命周期钩子可以更灵活地被组织和复用,而不必局限于选项式 API 中的
data、methods等。
总结
Vue 2 和 Vue 3 的生命周期钩子在功能上保持一致,但 Vue 3 引入的组合式 API 使得生命周期的使用更灵活、逻辑复用更简单。
2. Vue 的双向数据绑定原理
Details
Vue 的双向数据绑定是其核心特性之一,它使得数据模型与视图之间能够自动同步,无需手动操作 DOM。Vue 的双向数据绑定原理主要基于以下两个机制:
1. 数据劫持
Vue 2 使用 Object.defineProperty() 方法对数据对象的属性进行劫持,当属性被访问或修改时,会触发相应的 getter 和 setter 方法。
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('get:', key);
return val;
},
set(newVal) {
if (newVal === val) return;
console.log('set:', key, newVal);
val = newVal;
// 通知更新
}
});
}Vue 3 使用 Proxy 对象对数据对象进行代理,相比 Object.defineProperty(),Proxy 可以监听对象的所有属性,包括新增属性和删除属性。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('get:', key);
return target[key];
},
set(target, key, value) {
console.log('set:', key, value);
target[key] = value;
return true;
}
});
}2. 发布-订阅模式
Vue 使用发布-订阅模式来实现数据变化时的视图更新。当数据发生变化时,会通知所有订阅该数据的组件进行更新。
- Observer:观察者,负责监听数据变化并通知订阅者。
- Watcher:订阅者,负责接收数据变化的通知并更新视图。
- Dep:依赖收集器,负责管理订阅者列表。
双向绑定的实现
Vue 的双向数据绑定通过 v-model 指令实现,它是 v-bind 和 v-on 的语法糖:
<input v-model="message">等价于:
<input :value="message" @input="message = $event.target.value">总结
Vue 的双向数据绑定原理基于数据劫持和发布-订阅模式,通过 Object.defineProperty()(Vue 2)或 Proxy(Vue 3)对数据进行监听,当数据变化时,通知订阅者更新视图,从而实现数据模型与视图的自动同步。
3. Vue 的 computed 和 watch 的区别
Details
computed 和 watch 都是 Vue 中用于响应式数据处理的重要特性,但它们的使用场景和实现原理有所不同。
computed
定义:computed 是计算属性,用于根据依赖数据计算出一个新值。
特点:
- 缓存性:计算属性会缓存计算结果,只有当依赖的数据发生变化时,才会重新计算。
- 响应式:计算属性是响应式的,当依赖的数据变化时,计算属性会自动更新。
- 声明式:计算属性是声明式的,更适合用于模板中显示计算结果。
使用场景:
- 当需要根据多个数据计算出一个新值时
- 当需要缓存计算结果以提高性能时
- 当计算逻辑比较复杂时
示例:
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}watch
定义:watch 是监听器,用于监听数据变化并执行相应的操作。
特点:
- 响应式:监听器会监听数据变化并执行回调函数。
- 副作用:监听器适合执行副作用操作,如 API 请求、DOM 操作等。
- 配置选项:可以配置
deep(深度监听)和immediate(立即执行)等选项。
使用场景:
- 当需要在数据变化时执行异步操作或复杂逻辑时
- 当需要监听对象的深层属性变化时
- 当需要在组件初始化时立即执行监听逻辑时
示例:
watch: {
message: {
handler(newVal, oldVal) {
console.log('message changed:', newVal, oldVal);
},
deep: true,
immediate: true
}
}主要区别
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 有 | 无 |
| 同步 | 同步计算 | 可异步 |
| 用途 | 计算值 | 执行副作用 |
| 依赖 | 自动追踪依赖 | 需要手动指定依赖 |
| 返回值 | 必须返回值 | 无返回值 |
总结
computed 适合用于计算衍生值,具有缓存性,更高效;watch 适合用于执行副作用操作,如 API 请求、DOM 操作等。在实际开发中,应根据具体场景选择合适的方式。
4. Vue 的 v-if 和 v-show 的区别
Details
v-if 和 v-show 都是 Vue 中用于条件渲染的指令,但它们的实现原理和使用场景有所不同。
v-if
定义:v-if 是条件渲染指令,根据表达式的值决定是否渲染元素。
特点:
- 惰性:当条件为 false 时,元素不会被渲染到 DOM 中。
- 条件切换:当条件从 false 变为 true 时,会创建元素;当条件从 true 变为 false 时,会销毁元素。
- 性能:切换成本较高,适合不频繁切换的场景。
- 作用域:可以与
v-else和v-else-if配合使用,形成条件分支。
使用场景:
- 当条件不经常变化时
- 当条件为 false 时,不需要元素存在于 DOM 中时
- 当需要与
v-else和v-else-if配合使用时
示例:
<div v-if="isVisible">
这是 v-if 渲染的内容
</div>
<div v-else>
这是 v-else 渲染的内容
</div>v-show
定义:v-show 是条件显示指令,根据表达式的值决定是否显示元素。
特点:
- 非惰性:无论条件是否为 true,元素都会被渲染到 DOM 中。
- 条件切换:当条件变化时,通过修改元素的
displayCSS 属性来控制显示或隐藏。 - 性能:切换成本较低,适合频繁切换的场景。
- 作用域:不能与
v-else和v-else-if配合使用。
使用场景:
- 当条件频繁变化时
- 当条件为 false 时,元素可以存在于 DOM 中时
- 当需要快速切换元素的显示状态时
示例:
<div v-show="isVisible">
这是 v-show 渲染的内容
</div>主要区别
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件渲染,不满足条件时不渲染 | 始终渲染,通过 CSS 控制显示 |
| 切换成本 | 较高,需要创建/销毁元素 | 较低,只修改 CSS 属性 |
| 初始渲染 | 满足条件时才渲染 | 始终渲染 |
| 适用场景 | 不频繁切换的场景 | 频繁切换的场景 |
| 与 v-else 配合 | 可以 | 不可以 |
总结
v-if 适合用于不频繁切换的场景,因为它的切换成本较高,但初始渲染成本较低;v-show 适合用于频繁切换的场景,因为它的切换成本较低,但初始渲染成本较高。在实际开发中,应根据具体场景选择合适的指令。
5. Vue 的路由原理
Details
Vue Router 是 Vue 官方的路由管理器,它允许我们在单页应用中实现页面导航。Vue Router 的核心原理包括以下几个方面:
1. 路由模式
Vue Router 支持两种路由模式:
hash 模式:
- 使用 URL 的 hash 部分(# 后面的部分)来模拟路由
- 优点:兼容性好,不需要服务器配置
- 缺点:URL 中会包含 # 符号
history 模式:
- 使用 HTML5 History API 来实现路由
- 优点:URL 更美观,没有 # 符号
- 缺点:需要服务器配置,否则会出现 404 错误
2. 路由匹配
Vue Router 通过路由配置来匹配 URL 和组件:
const router = new VueRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
]
});当用户访问某个 URL 时,Vue Router 会根据路由配置找到对应的组件并渲染。
3. 路由导航
Vue Router 提供了多种导航方式:
声明式导航:
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>编程式导航:
// 跳转到指定路径
this.$router.push('/about');
// 替换当前路径
this.$router.replace('/about');
// 后退
this.$router.back();
// 前进
this.$router.forward();
// 跳转到指定历史记录
this.$router.go(-1);4. 路由守卫
Vue Router 提供了路由守卫来控制路由的访问权限:
全局守卫:
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查用户是否登录
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login');
} else {
next();
}
});
// 全局后置守卫
router.afterEach((to, from) => {
// 记录页面访问日志
console.log(`访问了 ${to.path}`);
});路由独享守卫:
const router = new VueRouter({
routes: [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
// 检查用户是否是管理员
if (!isAdmin) {
next('/');
} else {
next();
}
}
}
]
});组件内守卫:
export default {
beforeRouteEnter(to, from, next) {
// 在组件渲染前调用
},
beforeRouteUpdate(to, from, next) {
// 在当前路由更新时调用
},
beforeRouteLeave(to, from, next) {
// 在离开当前路由时调用
}
};5. 路由懒加载
为了提高应用的加载速度,Vue Router 支持路由懒加载:
const router = new VueRouter({
routes: [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
]
});路由懒加载会将每个路由对应的组件打包成单独的文件,只有当用户访问该路由时才会加载对应的文件,从而减少初始加载时间。
总结
Vue Router 的核心原理包括路由模式、路由匹配、路由导航、路由守卫和路由懒加载等。通过这些机制,Vue Router 实现了单页应用的页面导航功能,为用户提供了流畅的浏览体验。
6. Vuex 的原理和使用
Details
Vuex 是 Vue 官方的状态管理库,用于管理 Vue 应用中的共享状态。Vuex 的核心原理和使用方法如下:
1. 核心概念
Vuex 包含以下核心概念:
State:
- 存储应用的状态
- 是单一数据源
Getter:
- 从 State 中派生状态
- 类似于计算属性
Mutation:
- 修改 State 的唯一方式
- 必须是同步函数
Action:
- 处理异步操作
- 可以提交 Mutation
Module:
- 将 Store 分割成模块
- 每个模块有自己的 State、Getter、Mutation 和 Action
2. 基本使用
创建 Store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
export default store;在组件中使用:
// 访问 State
this.$store.state.count;
// 访问 Getter
this.$store.getters.doubleCount;
// 提交 Mutation
this.$store.commit('increment');
// 分发 Action
this.$store.dispatch('incrementAsync');使用辅助函数:
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['increment']),
...mapActions(['incrementAsync'])
}
};3. 工作原理
Vuex 的工作原理基于以下几点:
响应式:Vuex 的 State 是响应式的,当 State 发生变化时,所有依赖 State 的组件都会自动更新。
单向数据流:
- 组件通过 dispatch 触发 Action
- Action 通过 commit 触发 Mutation
- Mutation 修改 State
- State 的变化触发组件更新
严格模式:在严格模式下,任何直接修改 State 的行为都会抛出错误,确保所有 State 的修改都通过 Mutation 进行。
4. 模块系统
当应用变得复杂时,可以使用模块系统将 Store 分割成多个模块:
const userModule = {
namespaced: true,
state: {
user: null
},
getters: {
getUser: state => state.user
},
mutations: {
setUser(state, user) {
state.user = user;
}
},
actions: {
fetchUser({ commit }) {
// 异步获取用户信息
commit('setUser', { id: 1, name: 'John' });
}
}
};
const store = new Vuex.Store({
modules: {
user: userModule
}
});在组件中使用模块:
// 访问模块 State
this.$store.state.user.user;
// 访问模块 Getter
this.$store.getters['user/getUser'];
// 提交模块 Mutation
this.$store.commit('user/setUser', { id: 1, name: 'John' });
// 分发模块 Action
this.$store.dispatch('user/fetchUser');
// 使用辅助函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
computed: {
...mapState('user', ['user']),
...mapGetters('user', ['getUser'])
},
methods: {
...mapMutations('user', ['setUser']),
...mapActions('user', ['fetchUser'])
}
};总结
Vuex 是一个专为 Vue 应用设计的状态管理库,它通过 State、Getter、Mutation、Action 和 Module 等核心概念,实现了应用状态的集中管理和单向数据流。在大型应用中,Vuex 可以帮助我们更好地组织和管理状态,提高代码的可维护性。
7. Vue 的 mixin 和 composition API 的区别
Details
Vue 2 中的 mixin 和 Vue 3 中的 composition API 都是用于代码复用的重要特性,但它们的实现方式和使用场景有所不同。
mixin
定义:mixin 是 Vue 2 中用于代码复用的一种方式,它允许我们将组件的选项(如 data、methods、computed 等)提取到一个单独的对象中,然后在多个组件中复用。
使用方式:
// 定义 mixin
const myMixin = {
data() {
return {
message: 'Hello from mixin'
};
},
methods: {
greet() {
console.log(this.message);
}
},
mounted() {
this.greet();
}
};
// 在组件中使用 mixin
export default {
mixins: [myMixin],
data() {
return {
message: 'Hello from component'
};
}
};优点:
- 简单易用,适合小型项目
- 可以在多个组件中复用代码
缺点:
- 命名冲突:当组件和 mixin 有相同名称的选项时,组件的选项会覆盖 mixin 的选项(数据对象除外,数据对象会合并)
- 逻辑分散:mixin 中的逻辑分散在不同的选项中,难以追踪和维护
- 隐式依赖:mixin 和组件之间的依赖关系不明确,容易导致代码混乱
composition API
定义:composition API 是 Vue 3 中引入的一种新的代码组织方式,它允许我们将相关的逻辑组织在一起,而不是按照选项类型分散在不同的选项中。
使用方式:
// 定义 composable
import { ref, onMounted } from 'vue';
export function useGreeting() {
const message = ref('Hello from composable');
function greet() {
console.log(message.value);
}
onMounted(() => {
greet();
});
return {
message,
greet
};
}
// 在组件中使用 composable
import { useGreeting } from './useGreeting';
export default {
setup() {
const { message, greet } = useGreeting();
return {
message,
greet
};
}
};优点:
- 逻辑组织:相关的逻辑可以组织在一起,提高代码的可读性和可维护性
- 命名空间:通过返回值明确暴露给组件的状态和方法,避免命名冲突
- 类型支持:更好的 TypeScript 支持
- 逻辑复用:可以更灵活地复用逻辑,甚至在不同的组件之间共享逻辑
缺点:
- 学习成本:需要学习新的 API 和编程范式
- 代码量:对于简单的逻辑,使用 composition API 可能会增加代码量
主要区别
| 特性 | mixin | composition API |
|---|---|---|
| 代码组织 | 按选项类型组织 | 按逻辑功能组织 |
| 命名冲突 | 可能发生命名冲突 | 通过返回值避免命名冲突 |
| 依赖关系 | 隐式依赖 | 显式依赖 |
| 类型支持 | 较差 | 较好 |
| 逻辑复用 | 有限 | 灵活 |
| 可读性 | 较差 | 较好 |
| 维护性 | 较差 | 较好 |
总结
mixin 是 Vue 2 中用于代码复用的传统方式,适合小型项目;composition API 是 Vue 3 中引入的新方式,适合大型项目,提供了更好的代码组织和逻辑复用能力。在实际开发中,应根据项目的规模和复杂度选择合适的方式。
8. Vue 的性能优化技巧
Details
Vue 的性能优化是开发中非常重要的一环,以下是一些常见的 Vue 性能优化技巧:
1. 组件懒加载
路由懒加载:
const router = new VueRouter({
routes: [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
]
});组件懒加载:
export default {
components: {
LazyComponent: () => import('./LazyComponent.vue')
}
};2. 虚拟滚动
当需要渲染大量数据时,使用虚拟滚动可以显著提高性能:
<template>
<virtual-list
:data-key="'id'"
:data-sources="items"
:data-component="ItemComponent"
:estimate-size="54"
/>
</template>3. 合理使用 v-if 和 v-show
- v-if:适合不频繁切换的场景
- v-show:适合频繁切换的场景
4. 使用 computed 缓存计算结果
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}5. 使用 watch 的 deep 和 immediate 选项
watch: {
user: {
handler(newUser) {
// 处理用户变化
},
deep: true, // 深度监听
immediate: true // 立即执行
}
}6. 避免在模板中进行复杂计算
<!-- 不好的做法 -->
<div>{{ expensiveComputation() }}</div>
<!-- 好的做法 -->
<div>{{ computedValue }}</div>7. 使用 Object.freeze() 冻结数据
对于不需要响应式的数据,可以使用 Object.freeze() 来冻结,减少 Vue 的响应式开销:
data() {
return {
staticData: Object.freeze([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
};
}8. 合理使用 key
在使用 v-for 时,添加唯一的 key 可以帮助 Vue 更高效地更新 DOM:
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>9. 减少 watch 的使用
过多的 watch 会增加性能开销,尽量使用 computed 替代 watch。
10. 使用 keep-alive 缓存组件
对于频繁切换的组件,使用 keep-alive 可以缓存组件状态,避免重复渲染:
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>11. 优化图片加载
- 使用适当的图片格式
- 压缩图片
- 使用懒加载
12. 减少 DOM 元素数量
避免不必要的 DOM 元素,减少 DOM 树的深度。
13. 使用生产环境构建
在生产环境中,Vue 会自动进行一些优化,如删除调试信息、压缩代码等。
14. 使用 Webpack 或 Vite 的优化
- 代码分割
- 树摇
- 懒加载
总结
Vue 的性能优化需要从多个方面入手,包括组件懒加载、虚拟滚动、合理使用 v-if 和 v-show、使用 computed 缓存计算结果等。在实际开发中,应根据具体场景选择合适的优化策略,以提高应用的性能和用户体验。
9. Vue 3 的 composition API
Details
Vue 3 的 composition API 是 Vue 3 中引入的一种新的代码组织方式,它允许我们将相关的逻辑组织在一起,而不是按照选项类型分散在不同的选项中。
核心概念
setup() 函数:
- 是 composition API 的入口点
- 在组件创建之前执行
- 接收 props 和 context 作为参数
- 返回值会暴露给模板和其他选项
响应式 API:
ref():创建一个响应式的 ref 对象reactive():创建一个响应式的对象computed():创建一个计算属性watch():创建一个监听器watchEffect():创建一个自动追踪依赖的监听器
生命周期钩子:
onMounted():组件挂载后调用onUpdated():组件更新后调用onUnmounted():组件卸载后调用onBeforeMount():组件挂载前调用onBeforeUpdate():组件更新前调用onBeforeUnmount():组件卸载前调用onErrorCaptured():捕获子组件错误时调用onRenderTracked():响应式数据被追踪时调用onRenderTriggered():响应式数据变化时调用
基本使用
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
// 创建响应式数据
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
// 定义方法
function increment() {
count.value++;
}
// 使用生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
// 返回值暴露给模板
return {
count,
doubleCount,
increment
};
}
};优势
- 逻辑组织:相关的逻辑可以组织在一起,提高代码的可读性和可维护性
- 逻辑复用:可以更灵活地复用逻辑,甚至在不同的组件之间共享逻辑
- 类型支持:更好的 TypeScript 支持
- 树摇:未使用的代码可以被树摇掉,减少打包体积
- 更好的代码提示:IDE 可以提供更好的代码提示
与选项式 API 的对比
| 特性 | 选项式 API | composition API |
|---|---|---|
| 代码组织 | 按选项类型组织 | 按逻辑功能组织 |
| 逻辑复用 | 通过 mixin | 通过 composable |
| 类型支持 | 较差 | 较好 |
| 树摇 | 较差 | 较好 |
| 代码提示 | 一般 | 较好 |
| 学习成本 | 低 | 中 |
最佳实践
- 按功能组织逻辑:将相关的逻辑组织在一个 composable 中
- 使用 TypeScript:充分利用 TypeScript 的类型系统
- 保持 setup() 函数简洁:将复杂逻辑提取到 composable 中
- 使用 ref 和 reactive 合理:基本类型使用 ref,对象类型使用 reactive
- 避免在 setup() 中使用 this:setup() 函数中没有 this
总结
Vue 3 的 composition API 是一种新的代码组织方式,它提供了更好的逻辑组织和复用能力,以及更好的 TypeScript 支持。在大型项目中,composition API 可以显著提高代码的可维护性和可读性。
10. vue2和vue3的区别
Details
Vue 3 是 Vue.js 的重大版本更新,带来了许多新特性和改进。以下是 Vue 2 和 Vue 3 的主要区别:
1. 响应式系统
Vue 2:
- 使用
Object.defineProperty()实现响应式 - 无法监听对象的新增属性和删除属性
- 无法监听数组的索引和长度变化
Vue 3:
- 使用
Proxy实现响应式 - 可以监听对象的新增属性和删除属性
- 可以监听数组的索引和长度变化
- 性能更好,特别是在大型应用中
2. 组合式 API
Vue 2:
- 使用选项式 API,按选项类型组织代码
- 通过 mixin 实现代码复用
- 逻辑分散,难以维护
Vue 3:
- 引入组合式 API,按逻辑功能组织代码
- 通过 composable 实现代码复用
- 逻辑集中,易于维护
- 更好的 TypeScript 支持
3. 生命周期钩子
Vue 2:
beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed
Vue 3:
- 选项式 API 中的生命周期钩子名称基本不变
- 组合式 API 中使用函数形式的生命周期钩子,如
onMounted、onUpdated、onUnmounted等 beforeDestroy改名为beforeUnmountdestroyed改名为unmounted
4. 模板语法
Vue 3:
- 支持多根节点组件(Fragment)
- 支持
v-memo指令,用于缓存模板片段 - 支持
v-bind的简写形式.
5. 性能优化
Vue 3:
- 更高效的响应式系统
- 虚拟 DOM 重写,减少 patch 操作
- 静态提升,缓存静态节点
- 按需编译,减少运行时体积
- 树摇优化,减少打包体积
6. TypeScript 支持
Vue 3:
- 原生支持 TypeScript
- 更好的类型推断
- 组合式 API 提供更好的类型支持
7. 其他改进
Vue 3:
- 更好的错误处理
- 支持 Suspense 组件,用于处理异步组件
- 支持 Teleport 组件,用于将组件内容渲染到指定位置
- 支持 Fragment,允许组件有多个根节点
- 改进的自定义指令 API
- 改进的 provide/inject API
总结
Vue 3 带来了许多新特性和改进,包括更高效的响应式系统、组合式 API、更好的 TypeScript 支持等。这些改进使得 Vue 3 在性能、可维护性和开发体验方面都有显著提升。然而,Vue 3 也带来了一些破坏性变更,需要开发者进行相应的迁移。
11. vue2和vue3的响应式原理
Details
Vue 的响应式原理是其核心特性之一,Vue 2 和 Vue 3 在实现响应式系统方面有很大的不同。
Vue 2 的响应式原理
Vue 2 使用 Object.defineProperty() 方法对数据对象的属性进行劫持,当属性被访问或修改时,会触发相应的 getter 和 setter 方法。
基本实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 通知更新
dep.notify();
}
});
}依赖收集
Vue 2 使用 Dep 类来管理依赖:
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}观察者
Vue 2 使用 Watcher 类来观察数据变化:
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.expOrFn = expOrFn;
this.cb = cb;
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.expOrFn.call(this.vm);
Dep.target = null;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}局限性
- 无法监听对象的新增属性:需要使用
Vue.set()或this.$set()方法 - 无法监听对象的删除属性:需要使用
Vue.delete()或this.$delete()方法 - 无法监听数组的索引和长度变化:需要使用数组的变异方法(如 push、pop、splice 等)
- 性能问题:在大型对象中,遍历所有属性并添加 getter 和 setter 会影响性能
Vue 3 的响应式原理
Vue 3 使用 Proxy 对象对数据对象进行代理,相比 Object.defineProperty(),Proxy 可以监听对象的所有属性,包括新增属性和删除属性。
基本实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 收集依赖
track(target, key);
// 递归处理嵌套对象
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
// 通知更新
if (oldValue !== value) {
trigger(target, key, value, oldValue);
}
return result;
},
deleteProperty(target, key) {
const hasKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
// 通知更新
if (hasKey) {
trigger(target, key, undefined, Reflect.get(target, key, receiver));
}
return result;
}
});
}依赖收集和通知
Vue 3 使用 WeakMap 和 Map 来管理依赖:
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
function trigger(target, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
const effects = new Set(dep);
effects.forEach(effect => effect());
}
}优势
- 可以监听对象的新增属性:无需使用
Vue.set()方法 - 可以监听对象的删除属性:无需使用
Vue.delete()方法 - 可以监听数组的索引和长度变化:无需使用数组的变异方法
- 性能更好:
Proxy是浏览器原生支持的,性能比Object.defineProperty()更好 - 代码更简洁:
Proxy可以监听整个对象,而不需要遍历所有属性
总结
Vue 2 使用 Object.defineProperty() 实现响应式,存在一些局限性;Vue 3 使用 Proxy 实现响应式,解决了这些局限性,并且性能更好。两种实现方式各有优缺点,但 Vue 3 的响应式系统在功能和性能方面都有显著提升。
12. vue2和vue3的diff算法
Details
Vue 的 diff 算法是其虚拟 DOM 系统的核心,用于比较新旧虚拟 DOM 树的差异并高效更新真实 DOM。Vue 2 和 Vue 3 在 diff 算法的实现上有一些不同。
Vue 2 的 diff 算法
Vue 2 的 diff 算法基于以下原则:
- 同层比较:只比较同一层级的节点,不跨层级比较
- 先序遍历:按先序遍历的顺序比较节点
- key 匹配:使用 key 来匹配新旧节点
基本流程
- 初始化:创建新旧虚拟 DOM 树
- 比较根节点:如果根节点类型不同,直接替换整个子树
- 比较子节点:
- 如果新旧节点都有子节点,进行子节点比较
- 如果只有新节点有子节点,添加新节点
- 如果只有旧节点有子节点,删除旧节点
- 子节点比较:
- 使用双指针法,分别从新旧子节点的头部和尾部开始比较
- 找到可复用的节点,进行更新
- 处理剩余的节点,添加或删除
局限性
- 性能问题:在大型列表中,当节点位置发生变化时,diff 算法的性能会下降
- key 依赖:过度依赖 key 来匹配节点,如果 key 不稳定,会导致大量的 DOM 操作
Vue 3 的 diff 算法
Vue 3 的 diff 算法在 Vue 2 的基础上进行了优化,主要包括以下改进:
- 静态提升:缓存静态节点,避免重复比较
- 补丁标志:为节点添加补丁标志,只比较需要更新的部分
- 最长递增子序列:使用最长递增子序列算法来优化列表更新
基本流程
- 初始化:创建新旧虚拟 DOM 树
- 静态提升:识别并缓存静态节点
- 比较根节点:如果根节点类型不同,直接替换整个子树
- 比较子节点:
- 如果新旧节点都有子节点,进行子节点比较
- 如果只有新节点有子节点,添加新节点
- 如果只有旧节点有子节点,删除旧节点
- 子节点比较:
- 处理静态节点,直接复用
- 使用补丁标志,只比较需要更新的部分
- 对于列表,使用最长递增子序列算法来优化更新
优势
- 性能提升:通过静态提升和补丁标志,减少了不必要的比较和 DOM 操作
- 更高效的列表更新:使用最长递增子序列算法,减少了节点的移动操作
- 更好的类型支持:TypeScript 类型定义更完善
主要区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 静态提升 | 无 | 有 |
| 补丁标志 | 无 | 有 |
| 列表更新算法 | 双指针法 | 最长递增子序列 |
| 性能 | 一般 | 更好 |
| 代码复杂度 | 较低 | 较高 |
总结
Vue 3 的 diff 算法在 Vue 2 的基础上进行了优化,通过静态提升、补丁标志和最长递增子序列算法等技术,显著提高了虚拟 DOM 的更新性能。这些优化使得 Vue 3 在处理大型应用和复杂列表时表现更好。
13. vue2和vue3的虚拟dom
Details
虚拟 DOM 是 Vue 的核心概念之一,它是对真实 DOM 的抽象,用于提高 DOM 更新的性能。Vue 2 和 Vue 3 在虚拟 DOM 的实现上有一些不同。
Vue 2 的虚拟 DOM
Vue 2 的虚拟 DOM 是基于 Snabbdom 实现的,主要包括以下特点:
节点类型:
- 元素节点:表示 HTML 元素
- 文本节点:表示文本内容
- 注释节点:表示注释
节点结构:
javascript{ tag: 'div', // 标签名 data: {}, // 节点数据,如属性、事件等 children: [], // 子节点 text: undefined, // 文本内容 elm: undefined, // 对应的真实 DOM 元素 ns: undefined, // 命名空间 context: undefined, // 组件实例 functionalContext: undefined, // 函数式组件上下文 key: undefined, // 节点 key componentOptions: undefined, // 组件选项 componentInstance: undefined, // 组件实例 parent: undefined, // 父节点 raw: false, // 是否为原始 HTML isStatic: false, // 是否为静态节点 isRootInsert: true, // 是否为根节点插入 isComment: false, // 是否为注释节点 isCloned: false, // 是否为克隆节点 isOnce: false, // 是否为 v-once 节点 }渲染过程:
- 模板编译:将模板编译为渲染函数
- 渲染函数执行:生成虚拟 DOM 树
- diff 算法:比较新旧虚拟 DOM 树的差异
- 补丁操作:根据差异更新真实 DOM
Vue 3 的虚拟 DOM
Vue 3 的虚拟 DOM 在 Vue 2 的基础上进行了优化,主要包括以下改进:
节点类型:
- 元素节点:表示 HTML 元素
- 文本节点:表示文本内容
- 注释节点:表示注释
- 静态节点:表示静态内容
- 片段节点:表示多个根节点
节点结构:
javascript{ type: Symbol(Fragment), // 节点类型 props: {}, // 节点属性 children: [], // 子节点 patchFlag: 0, // 补丁标志 dynamicProps: null, // 动态属性 dynamicChildren: null, // 动态子节点 }优化:
- 静态提升:缓存静态节点,避免重复比较
- 补丁标志:为节点添加补丁标志,只比较需要更新的部分
- hoisting:将静态节点提升到渲染函数之外,避免重复创建
- cacheHandler:缓存事件处理器,避免重复创建
- SSR 优化:更好的服务端渲染支持
渲染过程:
- 模板编译:将模板编译为渲染函数,添加静态提升、补丁标志等优化
- 渲染函数执行:生成虚拟 DOM 树
- diff 算法:使用优化后的 diff 算法比较新旧虚拟 DOM 树的差异
- 补丁操作:根据差异更新真实 DOM
主要区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 节点结构 | 复杂 | 简洁 |
| 静态提升 | 无 | 有 |
| 补丁标志 | 无 | 有 |
| 片段节点 | 无 | 有 |
| 性能 | 一般 | 更好 |
| 代码复杂度 | 较低 | 较高 |
总结
Vue 3 的虚拟 DOM 在 Vue 2 的基础上进行了优化,通过静态提升、补丁标志等技术,显著提高了虚拟 DOM 的更新性能。这些优化使得 Vue 3 在处理大型应用和复杂列表时表现更好。
14. vue2和vue3的组件通信
Details
组件通信是 Vue 应用中的重要部分,Vue 2 和 Vue 3 在组件通信方式上有一些相同之处,也有一些不同之处。
1. Props / Emit
Vue 2:
// 父组件
<template>
<child :message="message" @update="handleUpdate" />
</template>
<script>
export default {
data() {
return {
message: 'Hello'
};
},
methods: {
handleUpdate(newMessage) {
this.message = newMessage;
}
}
};
</script>
// 子组件
<template>
<div>{{ message }}</div>
<button @click="updateMessage">Update</button>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
updateMessage() {
this.$emit('update', 'Hello from child');
}
}
};
</script>Vue 3:
// 父组件
<template>
<child :message="message" @update="handleUpdate" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const message = ref('Hello');
function handleUpdate(newMessage) {
message.value = newMessage;
}
</script>
// 子组件
<template>
<div>{{ message }}</div>
<button @click="updateMessage">Update</button>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
message: {
type: String,
default: ''
}
});
const emit = defineEmits(['update']);
function updateMessage() {
emit('update', 'Hello from child');
}
</script>2. $parent / $children
Vue 2:
// 父组件访问子组件
this.$children[0].method();
// 子组件访问父组件
this.$parent.method();Vue 3: Vue 3 中不再推荐使用 $parent 和 $children,而是使用 provide / inject 或 ref。
3. provide / inject
Vue 2:
// 父组件
export default {
provide() {
return {
message: 'Hello from parent'
};
}
};
// 子组件
export default {
inject: ['message']
};Vue 3:
// 父组件
<script setup>
import { provide } from 'vue';
provide('message', 'Hello from parent');
</script>
// 子组件
<script setup>
import { inject } from 'vue';
const message = inject('message');
</script>4. ref / $refs
Vue 2:
// 父组件
<template>
<child ref="childRef" />
</template>
<script>
export default {
mounted() {
this.$refs.childRef.method();
}
};
</script>Vue 3:
// 父组件
<template>
<child ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
const childRef = ref(null);
onMounted(() => {
childRef.value.method();
});
</script>5. Event Bus
Vue 2:
// 创建事件总线
const bus = new Vue();
// 发送事件
bus.$emit('event', data);
// 监听事件
bus.$on('event', data => {
console.log(data);
});Vue 3: Vue 3 中不再推荐使用 Event Bus,而是使用 mitt 或 tiny-emitter 等第三方库。
// 安装 mitt
// npm install mitt
// 创建事件总线
import mitt from 'mitt';
const bus = mitt();
// 发送事件
bus.emit('event', data);
// 监听事件
bus.on('event', data => {
console.log(data);
});6. Vuex
Vue 2:
// 安装 Vuex
// npm install vuex
// 创建 store
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
},
getters: {
doubleCount: state => state.count * 2
}
});
// 在组件中使用
this.$store.state.count;
this.$store.commit('increment');
this.$store.dispatch('incrementAsync');
this.$store.getters.doubleCount;Vue 3:
// 安装 Vuex
// npm install vuex@next
// 创建 store
import { createStore } from 'vuex';
const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
},
getters: {
doubleCount: state => state.count * 2
}
});
// 在组件中使用
import { useStore } from 'vuex';
const store = useStore();
store.state.count;
store.commit('increment');
store.dispatch('incrementAsync');
store.getters.doubleCount;7. Pinia
Vue 3: Vue 3 推荐使用 Pinia 作为状态管理库,它是 Vuex 的继任者,提供了更好的 TypeScript 支持和更简洁的 API。
// 安装 Pinia
// npm install pinia
// 创建 store
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
},
incrementAsync() {
setTimeout(() => {
this.count++;
}, 1000);
}
}
});
// 在组件中使用
import { useCounterStore } from './stores/counter';
const counterStore = useCounterStore();
counterStore.count;
counterStore.increment();
counterStore.incrementAsync();
counterStore.doubleCount;总结
Vue 2 和 Vue 3 在组件通信方式上有一些相同之处,如 Props / Emit、provide / inject、ref / $refs 等,也有一些不同之处,如 Vue 3 不再推荐使用 $parent / $children 和 Event Bus,而是推荐使用 provide / inject 或 Pinia 等。在实际开发中,应根据具体场景选择合适的组件通信方式。
15. vue2和vue3的keep-alive
Details
keep-alive 是 Vue 中的一个内置组件,用于缓存组件状态,避免重复渲染。Vue 2 和 Vue 3 在 keep-alive 的使用方式上有一些相同之处,也有一些不同之处。
Vue 2 的 keep-alive
基本使用:
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>属性:
- include:字符串或正则表达式,只有名称匹配的组件会被缓存
- exclude:字符串或正则表达式,名称匹配的组件不会被缓存
- max:数字,最多可以缓存多少个组件实例
示例:
<keep-alive include="Home,About">
<component :is="currentComponent"></component>
</keep-alive>
<keep-alive :max="10">
<component :is="currentComponent"></component>
</keep-alive>生命周期钩子:
- activated:组件被激活时调用
- deactivated:组件被停用时调用
示例:
export default {
activated() {
console.log('Component activated');
},
deactivated() {
console.log('Component deactivated');
}
};Vue 3 的 keep-alive
基本使用:
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>属性:
- include:字符串、正则表达式或数组,只有名称匹配的组件会被缓存
- exclude:字符串、正则表达式或数组,名称匹配的组件不会被缓存
- max:数字,最多可以缓存多少个组件实例
示例:
<keep-alive include="Home,About">
<component :is="currentComponent"></component>
</keep-alive>
<keep-alive :include="['Home', 'About']">
<component :is="currentComponent"></component>
</keep-alive>
<keep-alive :max="10">
<component :is="currentComponent"></component>
</keep-alive>生命周期钩子:
- onActivated:组件被激活时调用(组合式 API)
- onDeactivated:组件被停用时调用(组合式 API)
示例:
// 选项式 API
export default {
activated() {
console.log('Component activated');
},
deactivated() {
console.log('Component deactivated');
}
};
// 组合式 API
import { onActivated, onDeactivated } from 'vue';
export default {
setup() {
onActivated(() => {
console.log('Component activated');
});
onDeactivated(() => {
console.log('Component deactivated');
});
}
};主要区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 基本用法 | 相同 | 相同 |
| include/exclude | 字符串或正则表达式 | 字符串、正则表达式或数组 |
| max | 相同 | 相同 |
| 生命周期钩子 | activated/deactivated | 选项式 API 相同,组合式 API 使用 onActivated/onDeactivated |
| 性能 | 一般 | 更好 |
总结
Vue 2 和 Vue 3 在 keep-alive 的使用方式上基本相同,但 Vue 3 对 include 和 exclude 属性进行了扩展,支持数组格式,并且在组合式 API 中提供了 onActivated 和 onDeactivated 生命周期钩子。此外,Vue 3 对 keep-alive 的性能也进行了优化,使其在缓存组件时更加高效。
16. Vue 3 的 Teleport 组件
Details
Teleport 是 Vue 3 中引入的一个新组件,用于将组件的内容渲染到 DOM 树中的指定位置,而不是组件的父组件内部。
基本使用
<template>
<div class="container">
<h1>Main Content</h1>
<Teleport to="#modal-container">
<div class="modal">
<h2>Modal Content</h2>
<p>This is rendered in the modal container</p>
</div>
</Teleport>
</div>
</template>
<!-- 在 HTML 中 -->
<body>
<div id="app"></div>
<div id="modal-container"></div>
</body>特性
- to 属性:指定内容要渲染到的目标位置,可以是 CSS 选择器或 DOM 元素。
- disabled 属性:控制是否禁用 Teleport,当设置为 true 时,内容会渲染在原来的位置。
示例
基本用法:
<Teleport to="body">
<div class="fixed-overlay">
<p>Overlay content</p>
</div>
</Teleport>条件渲染:
<Teleport to="#modal-container" :disabled="!showModal">
<div v-if="showModal" class="modal">
<h2>Modal</h2>
<button @click="showModal = false">Close</button>
</div>
</Teleport>应用场景
- 模态框:将模态框内容渲染到 body 或其他指定容器,避免 z-index 问题。
- 通知组件:将通知消息渲染到页面顶部或其他固定位置。
- 悬浮组件:将悬浮内容渲染到指定位置,不受父组件样式影响。
注意事项
- 目标元素必须存在:Teleport 不会创建目标元素,只是将内容移动到已存在的元素中。
- 嵌套 Teleport:可以嵌套使用 Teleport,但要注意目标位置的正确性。
- 事件冒泡:Teleport 中的事件会正常冒泡到父组件,不受 teleport 位置的影响。
总结
Teleport 组件是 Vue 3 中一个非常实用的新特性,它允许我们将组件内容渲染到 DOM 树中的指定位置,解决了传统组件嵌套中的一些布局和样式问题。在实际开发中,Teleport 特别适合用于模态框、通知、悬浮组件等场景。
17. Vue 3 的 Suspense 组件
Details
Suspense 是 Vue 3 中引入的一个新组件,用于处理异步组件的加载状态。它可以在异步组件加载完成前显示一个加载状态,加载完成后显示组件内容。
基本使用
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
timeout: 3000 // 加载超时时间
});
export default {
components: {
AsyncComponent
}
};
</script>特性
- default 插槽:用于放置异步组件。
- fallback 插槽:用于在异步组件加载完成前显示的内容。
- 支持 await:可以在
setup()函数中使用await来等待异步操作完成。
示例
使用 defineAsyncComponent:
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingComponent, // 加载状态组件
errorComponent: ErrorComponent, // 错误状态组件
delay: 200, // 延迟显示加载状态的时间
timeout: 3000, // 加载超时时间
suspensible: true // 是否可挂起
});在 setup() 中使用 await:
// AsyncComponent.vue
<template>
<div>
<h2>{{ data.title }}</h2>
<p>{{ data.description }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 模拟异步数据获取
const fetchData = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({
title: 'Async Data',
description: 'This data was fetched asynchronously'
});
}, 2000);
});
};
// 在 setup() 中使用 await
const data = await fetchData();
</script>应用场景
- 异步组件加载:当组件需要从服务器加载时,可以使用
Suspense显示加载状态。 - 数据预加载:当组件需要获取数据后才能渲染时,可以使用
Suspense等待数据加载完成。 - 代码分割:结合
defineAsyncComponent实现代码分割,提高应用加载速度。
注意事项
- 只支持异步组件:
Suspense只能包裹异步组件或使用await的组件。 - 错误处理:需要单独处理异步操作的错误,可以使用
errorCaptured或onErrorCaptured。 - 嵌套使用:可以嵌套使用
Suspense组件,但要注意性能影响。
总结
Suspense 组件是 Vue 3 中一个处理异步操作的新特性,它可以让我们更优雅地处理异步组件的加载状态,提高用户体验。在实际开发中,Suspense 特别适合用于需要加载异步数据或异步组件的场景。
18. Vue Router 的动态路由和嵌套路由
Details
Vue Router 是 Vue 官方的路由管理器,它支持动态路由和嵌套路由,使我们可以构建更复杂的单页应用。
动态路由
基本概念: 动态路由是指路由路径中包含动态参数的路由,这些参数可以在组件中通过 $route.params 访问。
配置示例:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
props: true // 可以将路由参数作为 props 传递给组件
}
]
});使用示例:
// User.vue
<template>
<div>
<h1>User {{ id }}</h1>
</div>
</template>
<script>
export default {
props: ['id'], // 通过 props 接收路由参数
mounted() {
// 也可以通过 $route.params 访问
console.log(this.$route.params.id);
}
};
</script>路由参数变化: 当路由参数发生变化时,组件不会重新创建,而是会触发 beforeRouteUpdate 导航守卫:
export default {
beforeRouteUpdate(to, from, next) {
// 路由参数变化时的处理逻辑
this.id = to.params.id;
this.fetchData(this.id);
next();
}
};嵌套路由
基本概念: 嵌套路由是指在一个路由中嵌套另一个路由,形成路由层级结构。
配置示例:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功时,UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功时,UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
});使用示例:
<!-- User.vue -->
<template>
<div>
<h1>User {{ id }}</h1>
<router-view></router-view> <!-- 子路由会渲染在这里 -->
</div>
</template>路由守卫
全局守卫:
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查用户是否登录
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login');
} else {
next();
}
});路由独享守卫:
const router = new VueRouter({
routes: [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
// 检查用户是否是管理员
if (!isAdmin) {
next('/');
} else {
next();
}
}
}
]
});组件内守卫:
export default {
beforeRouteEnter(to, from, next) {
// 在组件渲染前调用
},
beforeRouteUpdate(to, from, next) {
// 在当前路由更新时调用
},
beforeRouteLeave(to, from, next) {
// 在离开当前路由时调用
}
};总结
Vue Router 的动态路由和嵌套路由是构建复杂单页应用的重要特性。动态路由允许我们处理参数化的路由,嵌套路由允许我们构建有层级结构的路由系统。结合路由守卫,我们可以实现更灵活、更安全的路由控制。
19. Pinia 和 Vuex 的区别
Details
Pinia 是 Vue 3 推荐的状态管理库,它是 Vuex 的继任者,提供了更好的 TypeScript 支持和更简洁的 API。
Pinia 的特点
- 简洁的 API:Pinia 的 API 更加简洁,不需要 mutations,直接在 actions 中修改状态。
- 更好的 TypeScript 支持:Pinia 原生支持 TypeScript,类型推断更加准确。
- 模块化设计:Pinia 的 store 是模块化的,每个 store 都是一个独立的模块。
- 无需命名空间:Pinia 的 store 自动具有命名空间,无需手动设置。
- 支持组合式 API:Pinia 可以与 Vue 3 的组合式 API 无缝集成。
- 轻量级:Pinia 的体积很小,只有约 1kb。
基本使用
创建 store:
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Pinia'
}),
getters: {
doubleCount: (state) => state.count * 2,
// 可以访问其他 getter
doubleCountPlusOne: (state, getters) => getters.doubleCount + 1
},
actions: {
increment() {
this.count++;
},
// 支持异步操作
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000));
this.count++;
}
}
});在组件中使用:
// 选项式 API
import { useCounterStore } from '@/stores/counter';
export default {
computed: {
...mapState(useCounterStore, ['count', 'name']),
...mapGetters(useCounterStore, ['doubleCount'])
},
methods: {
...mapActions(useCounterStore, ['increment', 'incrementAsync'])
}
};
// 组合式 API
import { useCounterStore } from '@/stores/counter';
import { computed } from 'vue';
export default {
setup() {
const counterStore = useCounterStore();
const count = computed(() => counterStore.count);
const doubleCount = computed(() => counterStore.doubleCount);
function increment() {
counterStore.increment();
}
function incrementAsync() {
counterStore.incrementAsync();
}
return {
count,
doubleCount,
increment,
incrementAsync
};
}
};与 Vuex 的区别
| 特性 | Vuex | Pinia |
|---|---|---|
| Mutations | 必须使用 mutations 修改状态 | 无需 mutations,直接在 actions 中修改状态 |
| TypeScript 支持 | 较差,需要手动添加类型 | 原生支持 TypeScript,类型推断准确 |
| 模块化 | 需要手动设置命名空间 | 自动具有命名空间 |
| 组合式 API 支持 | 较差 | 良好 |
| 体积 | 较大 | 较小(约 1kb) |
| 调试工具 | Vue DevTools | 支持 Vue DevTools |
| 生态系统 | 成熟 | 正在发展中 |
迁移策略
如果您正在从 Vuex 迁移到 Pinia,可以按照以下步骤进行:
- 安装 Pinia:
npm install pinia - 创建 Pinia 实例:在 main.js 中创建并使用 Pinia
- 迁移 store:将 Vuex 的 store 转换为 Pinia 的 store
- 更新组件:将组件中的 Vuex 代码更新为 Pinia 代码
- 测试:确保应用正常运行
总结
Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API、更好的 TypeScript 支持和更灵活的模块化设计。与 Vuex 相比,Pinia 更加轻量级,使用更加简单,是构建现代 Vue 应用的理想选择。
20. Vue 中的自定义指令
Details
Vue 允许我们创建自定义指令,用于对 DOM 元素进行底层操作。自定义指令可以在组件的模板中使用,就像内置指令一样。
基本语法
全局指令:
// 注册全局指令
Vue.directive('focus', {
// 指令绑定到元素时调用
bind(el, binding, vnode) {
// 做一次性的初始化设置
},
// 元素插入到 DOM 中时调用
inserted(el, binding, vnode) {
// 例如:聚焦元素
el.focus();
},
// 组件更新时调用
update(el, binding, vnode, oldVnode) {
// 根据绑定值的变化更新元素
},
// 组件更新完成后调用
componentUpdated(el, binding, vnode, oldVnode) {
// 组件更新完成后的操作
},
// 指令与元素解绑时调用
unbind(el, binding, vnode) {
// 清理工作
}
});局部指令:
export default {
directives: {
focus: {
// 指令定义
inserted(el) {
el.focus();
}
}
}
};指令钩子函数
- bind:指令绑定到元素时调用,只执行一次。
- inserted:元素插入到 DOM 中时调用。
- update:组件更新时调用,可能在子组件更新之前。
- componentUpdated:组件更新完成后调用。
- unbind:指令与元素解绑时调用,只执行一次。
钩子函数参数
- el:指令绑定的元素,可以直接操作 DOM。
- binding:一个对象,包含以下属性:
- name:指令名称,不包括 v- 前缀。
- value:指令的绑定值。
- oldValue:指令的前一个绑定值,仅在 update 和 componentUpdated 钩子中可用。
- expression:指令的表达式。
- arg:指令的参数。
- modifiers:指令的修饰符。
- vnode:Vue 编译生成的虚拟节点。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
示例
自定义指令 v-focus:
// 全局注册
Vue.directive('focus', {
inserted(el) {
el.focus();
}
});
// 在模板中使用
<template>
<input v-focus type="text">
</template>自定义指令 v-color:
// 全局注册
Vue.directive('color', {
bind(el, binding) {
el.style.color = binding.value;
},
update(el, binding) {
el.style.color = binding.value;
}
});
// 在模板中使用
<template>
<div v-color="color">Hello World</div>
</template>
<script>
export default {
data() {
return {
color: 'red'
};
}
};
</script>带参数的自定义指令:
// 全局注册
Vue.directive('position', {
bind(el, binding) {
el.style.position = 'fixed';
el.style[binding.arg] = binding.value + 'px';
}
});
// 在模板中使用
<template>
<div v-position:top="100">Hello World</div>
</template>应用场景
- 表单元素聚焦:自动聚焦到输入框。
- 滚动到底部:聊天应用中自动滚动到最新消息。
- 拖拽功能:实现元素的拖拽功能。
- 图片懒加载:延迟加载图片。
- 权限控制:根据用户权限显示或隐藏元素。
总结
自定义指令是 Vue 中一个强大的特性,它允许我们对 DOM 元素进行底层操作,实现一些内置指令无法实现的功能。在实际开发中,自定义指令可以帮助我们封装一些常用的 DOM 操作,提高代码的复用性和可维护性。
21. Vue 中的插槽(Slot)
Details
插槽是 Vue 中用于组件内容分发的机制,它允许我们在父组件中向子组件传递内容,实现组件的复用和定制。
基本插槽
子组件:
<!-- Child.vue -->
<template>
<div class="child">
<h2>Child Component</h2>
<slot></slot> <!-- 插槽位置 -->
</div>
</template>父组件:
<!-- Parent.vue -->
<template>
<div class="parent">
<h1>Parent Component</h1>
<Child>
<p>This is content passed to the slot</p>
</Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
components: {
Child
}
};
</script>具名插槽
子组件:
<!-- Child.vue -->
<template>
<div class="child">
<header>
<slot name="header"></slot> <!-- 具名插槽 -->
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot> <!-- 具名插槽 -->
</footer>
</div>
</template>父组件:
<!-- Parent.vue -->
<template>
<div class="parent">
<h1>Parent Component</h1>
<Child>
<template v-slot:header>
<h2>Header Content</h2>
</template>
<p>Main Content</p>
<template v-slot:footer>
<p>Footer Content</p>
</template>
</Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
components: {
Child
}
};
</script>缩写语法:
<Child>
<template #header>
<h2>Header Content</h2>
</template>
<p>Main Content</p>
<template #footer>
<p>Footer Content</p>
</template>
</Child>作用域插槽
子组件:
<!-- Child.vue -->
<template>
<div class="child">
<h2>Child Component</h2>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item">{{ item.name }}</slot> <!-- 作用域插槽,传递 item 数据 -->
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
};
}
};
</script>父组件:
<!-- Parent.vue -->
<template>
<div class="parent">
<h1>Parent Component</h1>
<Child>
<template v-slot:default="slotProps">
<!-- 使用 slotProps 访问子组件传递的数据 -->
{{ slotProps.item.id }}: {{ slotProps.item.name }}
</template>
</Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
components: {
Child
}
};
</script>解构语法:
<Child>
<template v-slot:default="{ item }">
<!-- 解构 slotProps -->
{{ item.id }}: {{ item.name }}
</template>
</Child>动态插槽名
父组件:
<template>
<div class="parent">
<h1>Parent Component</h1>
<Child>
<template v-slot:[dynamicSlotName]>
<p>Dynamic Slot Content</p>
</template>
</Child>
</div>
</template>
<script>
export default {
data() {
return {
dynamicSlotName: 'header'
};
},
components: {
Child
}
};
</script>应用场景
- 布局组件:创建可定制的布局组件,如页面头部、主体、底部。
- 列表组件:创建可定制的列表项组件。
- 卡片组件:创建可定制的卡片组件,如卡片头部、内容、底部。
- 表单组件:创建可定制的表单组件,如表单字段标签、输入框、错误信息。
总结
插槽是 Vue 中一个强大的特性,它允许我们在父组件中向子组件传递内容,实现组件的复用和定制。通过基本插槽、具名插槽和作用域插槽,我们可以创建更加灵活和可复用的组件。
22. Vue 中的异步组件
Details
异步组件是 Vue 中用于延迟加载组件的机制,它可以减少初始加载时间,提高应用性能。
基本用法
Vue 2:
// 全局注册
Vue.component('AsyncComponent', function(resolve, reject) {
// 异步加载组件
setTimeout(() => {
resolve({
template: '<div>Async Component</div>'
});
}, 1000);
});
// 局部注册
export default {
components: {
AsyncComponent: function(resolve, reject) {
setTimeout(() => {
resolve({
template: '<div>Async Component</div>'
});
}, 1000);
}
}
};Vue 3:
import { defineAsyncComponent } from 'vue';
// 基本用法
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
timeout: 3000 // 加载超时时间
});
// 简化用法
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
// 局部注册
export default {
components: {
AsyncComponent
}
};高级选项
Vue 3:
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingComponent, // 加载状态组件
errorComponent: ErrorComponent, // 错误状态组件
delay: 200, // 延迟显示加载状态的时间(毫秒)
timeout: 3000, // 加载超时时间(毫秒)
suspensible: true, // 是否可挂起,与 Suspense 组件配合使用
onError(error, retry, fail, attempts) {
// 错误处理函数
if (attempts <= 3) {
// 最多重试 3 次
retry();
} else {
fail();
}
}
});与 Suspense 配合使用
Vue 3:
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
export default {
components: {
AsyncComponent
}
};
</script>应用场景
- 大型组件:延迟加载体积较大的组件,如地图、图表等。
- 路由懒加载:与 Vue Router 配合使用,实现路由的懒加载。
- 条件渲染:只在需要时加载组件,如用户点击某个按钮后才加载的组件。
- 代码分割:将应用代码分割成多个小块,减少初始加载时间。
路由懒加载示例
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
},
{
path: '/user/:id',
component: () => import('./views/User.vue')
}
]
});
export default router;总结
异步组件是 Vue 中用于延迟加载组件的机制,它可以减少初始加载时间,提高应用性能。在 Vue 3 中,defineAsyncComponent 函数提供了更灵活的配置选项,与 Suspense 组件配合使用可以实现更优雅的加载状态管理。
23. Vue 中的依赖注入(provide/inject)
Details
依赖注入是 Vue 中用于组件间通信的一种机制,它允许父组件向其所有子组件(包括深层子组件)提供数据和方法,而无需通过 props 层层传递。
基本用法
父组件:
// Vue 2
export default {
provide() {
return {
message: 'Hello from parent',
method: this.someMethod
};
},
methods: {
someMethod() {
console.log('Method called from parent');
}
}
};
// Vue 3 - 选项式 API
export default {
provide() {
return {
message: 'Hello from parent',
method: this.someMethod
};
},
methods: {
someMethod() {
console.log('Method called from parent');
}
}
};
// Vue 3 - 组合式 API
import { provide } from 'vue';
export default {
setup() {
const message = 'Hello from parent';
function someMethod() {
console.log('Method called from parent');
}
provide('message', message);
provide('method', someMethod);
}
};子组件:
// Vue 2
export default {
inject: ['message', 'method'],
mounted() {
console.log(this.message); // 输出: Hello from parent
this.method(); // 输出: Method called from parent
}
};
// Vue 3 - 选项式 API
export default {
inject: ['message', 'method'],
mounted() {
console.log(this.message); // 输出: Hello from parent
this.method(); // 输出: Method called from parent
}
};
// Vue 3 - 组合式 API
import { inject } from 'vue';
export default {
setup() {
const message = inject('message');
const method = inject('method');
console.log(message); // 输出: Hello from parent
method(); // 输出: Method called from parent
return {
message
};
}
};带默认值的注入
子组件:
// Vue 2
export default {
inject: {
message: {
default: 'Default message'
},
method: {
default: function() {
console.log('Default method');
}
}
}
};
// Vue 3 - 选项式 API
export default {
inject: {
message: {
default: 'Default message'
},
method: {
default: function() {
console.log('Default method');
}
}
}
};
// Vue 3 - 组合式 API
import { inject } from 'vue';
export default {
setup() {
const message = inject('message', 'Default message');
const method = inject('method', () => {
console.log('Default method');
});
return {
message
};
}
};响应式注入
父组件:
// Vue 3 - 组合式 API
import { provide, ref } from 'vue';
export default {
setup() {
const count = ref(0);
function increment() {
count.value++;
}
provide('count', count);
provide('increment', increment);
return {
count,
increment
};
}
};子组件:
// Vue 3 - 组合式 API
import { inject } from 'vue';
export default {
setup() {
const count = inject('count');
const increment = inject('increment');
return {
count,
increment
};
}
};应用场景
- 主题配置:在应用根组件中提供主题配置,所有子组件都可以访问。
- 用户信息:在应用根组件中提供用户信息,所有子组件都可以访问。
- 全局状态:在应用根组件中提供全局状态,所有子组件都可以访问。
- 服务:在应用根组件中提供服务(如 API 服务),所有子组件都可以使用。
注意事项
- 响应式:如果提供的值是响应式的(如 ref 或 reactive),那么注入的值也是响应式的。
- 作用域:provide/inject 只在父组件及其子组件之间有效,不影响其他组件树。
- 命名冲突:如果多个父组件提供了相同名称的注入,子组件会接收最近的父组件提供的值。
- 类型安全:在 TypeScript 中,需要为注入的值添加类型注解。
总结
依赖注入是 Vue 中用于组件间通信的一种机制,它允许父组件向其所有子组件提供数据和方法,而无需通过 props 层层传递。在 Vue 3 中,provide/inject API 与组合式 API 无缝集成,使用更加灵活。
24. Vue 中的错误处理
Details
错误处理是 Vue 应用中重要的一环,它可以帮助我们捕获和处理应用中的错误,提高应用的稳定性和用户体验。
错误处理策略
- 全局错误处理:捕获整个应用的错误。
- 组件级错误处理:捕获特定组件的错误。
- 异步错误处理:捕获异步操作(如 API 请求)的错误。
- 路由错误处理:捕获路由导航过程中的错误。
全局错误处理
Vue 2:
// 全局错误处理
Vue.config.errorHandler = function(err, vm, info) {
console.error('Vue 错误:', err);
console.error('组件:', vm);
console.error('错误信息:', info);
// 可以在这里添加错误上报逻辑
};
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的 Promise 错误:', event.reason);
// 可以在这里添加错误上报逻辑
});
// 未捕获的错误
window.addEventListener('error', function(event) {
console.error('未捕获的错误:', event.error);
// 可以在这里添加错误上报逻辑
});Vue 3:
// 全局错误处理
app.config.errorHandler = function(err, instance, info) {
console.error('Vue 错误:', err);
console.error('组件实例:', instance);
console.error('错误信息:', info);
// 可以在这里添加错误上报逻辑
};
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的 Promise 错误:', event.reason);
// 可以在这里添加错误上报逻辑
});
// 未捕获的错误
window.addEventListener('error', function(event) {
console.error('未捕获的错误:', event.error);
// 可以在这里添加错误上报逻辑
});组件级错误处理
Vue 2:
export default {
errorCaptured(err, vm, info) {
console.error('组件错误:', err);
console.error('子组件:', vm);
console.error('错误信息:', info);
// 可以在这里添加错误处理逻辑
// 返回 true 可以阻止错误继续向上传播
return false;
}
};Vue 3 - 选项式 API:
export default {
errorCaptured(err, instance, info) {
console.error('组件错误:', err);
console.error('子组件实例:', instance);
console.error('错误信息:', info);
// 可以在这里添加错误处理逻辑
// 返回 true 可以阻止错误继续向上传播
return false;
}
};Vue 3 - 组合式 API:
import { onErrorCaptured } from 'vue';
export default {
setup() {
onErrorCaptured((err, instance, info) => {
console.error('组件错误:', err);
console.error('子组件实例:', instance);
console.error('错误信息:', info);
// 可以在这里添加错误处理逻辑
// 返回 true 可以阻止错误继续向上传播
return false;
});
}
};异步错误处理
使用 try/catch:
export default {
methods: {
async fetchData() {
try {
const response = await axios.get('/api/data');
this.data = response.data;
} catch (error) {
console.error('API 请求错误:', error);
this.error = '获取数据失败,请重试';
}
}
}
};使用 Promise.catch:
export default {
methods: {
fetchData() {
axios.get('/api/data')
.then(response => {
this.data = response.data;
})
.catch(error => {
console.error('API 请求错误:', error);
this.error = '获取数据失败,请重试';
});
}
}
};路由错误处理
Vue Router:
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由错误处理
router.onError(error => {
console.error('路由错误:', error);
// 可以在这里添加错误处理逻辑
});
export default router;错误边界组件
Vue 3:
// ErrorBoundary.vue
<template>
<div v-if="error" class="error-boundary">
<h2>发生错误</h2>
<p>{{ error.message }}</p>
<button @click="resetError">重试</button>
</div>
<slot v-else></slot>
</template>
<script>
import { ref, onErrorCaptured } from 'vue';
export default {
setup(props, { slots }) {
const error = ref(null);
const resetError = () => {
error.value = null;
};
onErrorCaptured((err) => {
error.value = err;
return true; // 阻止错误继续向上传播
});
return {
error,
resetError
};
}
};
</script>
// 使用
<template>
<ErrorBoundary>
<ComponentThatMightError />
</ErrorBoundary>
</template>
<script>
import ErrorBoundary from './ErrorBoundary.vue';
import ComponentThatMightError from './ComponentThatMightError.vue';
export default {
components: {
ErrorBoundary,
ComponentThatMightError
}
};
</script>总结
错误处理是 Vue 应用中重要的一环,它可以帮助我们捕获和处理应用中的错误,提高应用的稳定性和用户体验。通过全局错误处理、组件级错误处理、异步错误处理和路由错误处理,我们可以构建更加健壮的 Vue 应用。
25. Vue 3 的 Fragment 组件
Details
Fragment 是 Vue 3 中引入的一个新特性,它允许组件拥有多个根节点,而不需要像 Vue 2 那样只能有一个根节点。
基本使用
Vue 2(只能有一个根节点):
<template>
<div> <!-- 必须有一个根节点 -->
<h1>Hello</h1>
<p>World</p>
</div>
</template>Vue 3(可以有多个根节点):
<template>
<h1>Hello</h1> <!-- 第一个根节点 -->
<p>World</p> <!-- 第二个根节点 -->
</template>优势
- 减少不必要的 DOM 元素:不需要为了满足单根节点的要求而添加额外的包装元素。
- 更灵活的组件结构:可以创建更自然、更符合语义的组件结构。
- 更好的 CSS 布局:避免了额外的包装元素对 CSS 布局的影响。
- 更简洁的模板:模板代码更加简洁,可读性更好。
注意事项
- 属性传递:当组件有多个根节点时,属性不会自动传递到子节点,需要显式指定。
<template>
<h1 :class="titleClass">Hello</h1>
<p>World</p>
</template>
<script>
export default {
props: ['titleClass']
};
</script>- 事件处理:当组件有多个根节点时,事件不会自动绑定到子节点,需要显式指定。
<template>
<button @click="handleClick">Click me</button>
<p>World</p>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('Button clicked');
}
}
};
</script>应用场景
- 列表项组件:创建包含多个元素的列表项组件。
- 表单控件组件:创建包含标签、输入框和错误信息的表单控件组件。
- 布局组件:创建包含多个布局元素的布局组件。
- 条件渲染:根据条件渲染不同的根节点。
总结
Fragment 是 Vue 3 中一个非常实用的新特性,它允许组件拥有多个根节点,减少了不必要的 DOM 元素,使组件结构更加灵活和自然。在实际开发中,Fragment 可以帮助我们创建更简洁、更符合语义的组件。