前端面试 Vue 篇
1. 对 MVVM 的理解
MVVM(Model-View-ViewModel)是一种软件架构设计模式,最早由微软在开发 WPF(Windows Presentation Foundation)和 Silverlight 框架时提出,广泛应用于前端开发,尤其是 Vue、Angular 等前端框架。
MVVM 的三层结构
Model(模型):
主要负责数据的获取和处理,它通常是指业务数据的模型,包含应用的数据结构和业务逻辑。Model 中的任何变化都会通过数据绑定自动更新到 View 层。View(视图):
View 负责用户界面的呈现,它在 MVVM 中是完全“哑”的,也就是不包含任何业务逻辑,只负责展示数据和响应用户的操作。ViewModel(视图模型):
ViewModel 是 MVVM 模式的核心。它连接了 View 和 Model,通过双向数据绑定来实现数据的同步,起到了“中介”的作用。ViewModel 中包含了和视图交互的逻辑,它将 Model 中的数据以视图友好的格式提供给 View,同样也可以响应 View 中的数据修改并更新 Model。
MVVM 的优点
- 解耦视图和业务逻辑:通过将视图的展示和业务逻辑分开,提升代码的可读性和可维护性。
- 双向数据绑定:Model 和 View 的数据自动同步,减少了手动 DOM 操作,提高了开发效率。
- 便于测试:因为 ViewModel 中没有直接依赖 View 的具体实现,业务逻辑的单元测试更加方便。
MVVM 的缺点
- 复杂性:在较为简单的应用中,MVVM 的引入可能增加不必要的复杂性,反而不利于开发效率。
- 内存消耗:双向绑定在大型应用中可能导致性能问题和较高的内存消耗,特别是在有大量数据需要双向绑定时。
总结
MVVM 提供了一种良好的前端开发思路,通过数据驱动视图,可以有效提升开发效率和用户体验。
2. 说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA(Single Page Application,单页面应用)是一种网页应用结构。与传统的多页面应用(MPA,Multi-Page Application)不同,SPA 只有一个 HTML 页面,并在用户操作时动态更新页面内容,而无需重新加载整个页面。
SPA 通过 JavaScript 在前端处理路由和页面渲染,通常会使用 Ajax 请求和 API 获取数据,从而避免了频繁的页面跳转,使用户获得更流畅的体验。Vue、React、Angular 等前端框架通常都支持 SPA 的开发模式。
SPA 的工作流程
- 用户首次访问页面时,服务器返回一个包含 HTML、CSS、JavaScript 的初始页面。
- 之后的用户操作不再请求新页面,而是通过 JavaScript 来动态更新页面内容。
- 前端的路由管理会监听 URL 的变化(通常通过 Hash 或 History API),并根据路径加载相应的内容。
- 数据通过 Ajax 或 API 请求从服务器获取并渲染到页面上。
SPA 的优点
- 用户体验流畅:页面只需加载一次,之后的操作都在页面内完成,避免了传统页面的跳转和重新加载。
- 前后端分离:SPA 通过 API 与后端通信,前端和后端可以各自独立开发,利于代码的解耦。
- 性能提升:在内容不变的情况下,页面只需局部更新,不必重新加载整个页面,减轻了服务器负担,提高了响应速度。
- 易于构建移动端体验:SPA 可以较为方便地转化为 PWA(渐进式 Web 应用),从而提供更接近原生应用的体验。
SPA 的缺点
- 首次加载时间较长:由于所有资源在首次加载时都要获取,会导致页面初始加载速度较慢,影响用户体验,尤其在网速较慢的情况下。
- SEO 难度较高:因为页面内容是通过 JavaScript 动态渲染的,搜索引擎很难爬取到有效信息,SEO 需要额外处理(如通过 SSR 或预渲染)。
- 内存管理问题:由于页面一直处于“活动”状态,内存消耗较大,特别是在复杂页面或使用了大量动态数据时,可能导致内存泄漏。
- 前端路由复杂性:需要在前端管理路由,开发和维护复杂的路由逻辑可能会增加代码复杂度。
总结
SPA 适合需要更强用户交互和流畅体验的应用,例如社交媒体、后台管理系统等。但在首屏加载速度、SEO 友好性上仍有不足,适合根据应用需求进行取舍。
3. Vue 2 与 Vue 3 生命周期对比及功能说明
Vue 2 和 Vue 3 的生命周期钩子大致相同,但 Vue 3 对部分钩子名称和触发机制进行了调整,并新增了一些调试相关的钩子。以下是 Vue 2 和 Vue 3 主要生命周期的对比和每个生命周期的作用说明。
生命周期对比及说明
Vue3 setup():组件创建之前
Vue 2 生命周期钩子 | Vue 3 生命周期钩子(选项式 API) | Vue 3 生命周期钩子(Composition API) | 功能说明 |
---|---|---|---|
beforeCreate | beforeCreate | onBeforeMount | 实例初始化之前调用,数据观测和事件初始化尚未完成,通常不常用。 |
created | created | onMounted | 实例创建完成,此时可以访问数据和方法,DOM 尚未渲染,常用于数据初始化。 |
beforeMount | beforeMount | onBeforeUpdate | 模板编译结束并将要挂载到页面上,DOM 尚未插入,可以在此阶段做挂载前的准备工作。 |
mounted | mounted | onUpdated | 实例挂载完成,DOM 已插入页面。常用于 DOM 操作或与其他库的交互。 |
beforeUpdate | beforeUpdate | onBeforeUnmount | 数据更新但 DOM 尚未渲染更新,可用于在数据更新前执行操作,比如清除无效的状态。 |
updated | updated | onUnmounted | 数据更新且 DOM 重新渲染完成,适合执行基于最新 DOM 的操作。 |
activated | activated | onActivated | 在 <keep-alive> 缓存组件激活时调用,常用于处理缓存组件的状态恢复。 |
deactivated | deactivated | onDeactivated | 在 <keep-alive> 缓存组件停用时调用,适合用于保存组件状态。 |
beforeDestroy | 替换为 beforeUnmount | onBeforeUnmount | 实例销毁前调用,可用于清除计时器、事件监听等,避免内存泄漏。 |
destroyed | 替换为 unmounted | onUnmounted | 实例销毁后调用,此时所有事件监听和响应式数据解绑,适合用于资源释放。 |
无 | errorCaptured | onErrorCaptured | 捕获子组件中的错误,常用于错误处理和日志记录。 |
无 | renderTracked | onRenderTracked | 在响应式依赖被追踪时调用,主要用于调试,查看哪些状态引发了渲染。 |
无 | renderTriggered | onRenderTriggered | 在渲染被触发时调用,主要用于调试,帮助分析哪些状态变动引发了渲染。 |
主要功能变化
销毁阶段钩子名称变化
- Vue 2 中的
beforeDestroy
和destroyed
被替换为 Vue 3 中的beforeUnmount
和unmounted
,更贴合实例的挂载和卸载过程。
- Vue 2 中的
Composition API 的灵活钩子
Vue 3 提供了onXxx
系列钩子函数,在 Composition API 中可以直接调用,如onMounted
、onUpdated
,方便逻辑复用。尤其适合在逻辑复杂、组件拆分较细的场景中使用。新增调试相关钩子
renderTracked
:追踪响应式依赖,帮助开发者了解渲染过程中依赖的变化。renderTriggered
:渲染被触发时调用,可用于分析引发渲染的原因和依赖关系,辅助性能优化。
结论
Vue 3 在保留 Vue 2 主要生命周期功能的同时,增强了生命周期的灵活性和调试能力,特别是在 Composition API 中提供的钩子,可以让生命周期管理更加便捷和可维护。
4.子组件以及父组件更新触发生命周期的过程
在 Vue 中,父组件和子组件的生命周期钩子会按一定的顺序触发。理解父子组件的生命周期触发顺序有助于避免不必要的重渲染和数据问题。
组件挂载过程中的生命周期触发顺序
当父组件首次挂载并渲染子组件时,生命周期钩子的触发顺序如下:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
总结:父组件的初始化钩子(beforeCreate
到 beforeMount
)会先于子组件的钩子执行;子组件的 mounted
先于父组件的 mounted
执行。
组件更新过程中的生命周期触发顺序
当父组件或子组件中的响应式数据发生变化时,更新过程中的生命周期触发顺序会略有不同:
1. 父组件数据变化导致父组件和子组件更新
当父组件的数据变化,并且影响到子组件的内容时,钩子触发顺序如下:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
总结:父组件的 beforeUpdate
钩子先触发,然后进入子组件的 beforeUpdate
、updated
,最后是父组件的 updated
钩子。这保证了子组件的更新在父组件完成之前完成。
2. 子组件数据变化导致子组件更新
当子组件自身的数据变化时,触发的生命周期如下:
- 子组件 beforeUpdate
- 子组件 updated
总结:如果只是子组件的数据变化,不会影响父组件,因此只触发子组件的 beforeUpdate
和 updated
钩子。
组件销毁过程中的生命周期触发顺序
当父组件被卸载(或条件渲染下某个子组件被卸载)时,生命周期钩子触发顺序如下:
- 父组件 beforeUnmount
- 子组件 beforeUnmount
- 子组件 unmounted
- 父组件 unmounted
总结:父组件在销毁阶段先触发 beforeUnmount
,然后是子组件的 beforeUnmount
和 unmounted
钩子,最后再触发父组件的 unmounted
钩子。
关键点总结
- 挂载阶段:父组件的
beforeMount
先触发,最后父组件的mounted
执行。 - 更新阶段:父组件的
beforeUpdate
先触发,子组件更新完成后再触发父组件的updated
。 - 销毁阶段:父组件的
beforeUnmount
先触发,子组件完全销毁后再触发父组件的unmounted
。
理解这一顺序有助于合理组织父子组件的数据流和更新逻辑,避免不必要的渲染和逻辑错误。
5. Vue 的响应式原理(Vue 2 vs Vue 3)
Vue 的响应式系统是其核心功能之一,它使得数据变化可以自动驱动视图更新,从而实现“数据驱动视图”的开发模式。Vue 2 和 Vue 3 的响应式实现有一些显著的差异,主要体现在性能优化和实现方式上。
Vue 2 的响应式原理
Vue 2 使用 Object.defineProperty()
来实现响应式。主要原理如下:
数据劫持
Vue 2 通过Object.defineProperty
劫持每个数据属性的get
和set
方法,在访问或修改数据时触发相关逻辑。依赖收集
每个响应式属性都有一个Dep
实例来存储该属性的依赖(即观察者)。当组件渲染时,会触发属性的get
方法,将依赖(Watcher
)收集到Dep
中。数据更新
当属性的set
方法被调用时,Dep
通知所有依赖该属性的Watcher
,从而触发视图更新。
Vue 2 响应式的局限
- 数组和对象的深层次监听问题
Object.defineProperty
不能监听对象和数组的新增或删除,需要 Vue 提供额外的方法(如Vue.set
和Vue.delete
)来解决。 - 性能问题
在大规模数据处理时,需要递归遍历整个对象树来进行深度监听,容易导致性能问题。
Vue 3 的响应式原理
Vue 3 使用 Proxy
实现响应式系统,这种实现方式更加灵活,克服了 Object.defineProperty
的局限性。主要原理如下:
响应式转换
Vue 3 使用Proxy
代理整个对象,将get
和set
操作委托给代理,从而可以直接监听新增和删除的属性。依赖收集和触发
在get
拦截器中进行依赖收集,当某个属性被访问时,会将依赖存储在一个全局的WeakMap
中。WeakMap
的键是响应式对象,值是存储依赖的Map
。Map
的键是对象的属性名,值是该属性的依赖集合(即Set
,用于存储Effect
)。
性能优化
Proxy
不需要递归整个对象即可监听嵌套属性,只有当嵌套属性被访问时才会创建代理,从而减少了性能开销。
Vue 3 响应式的优点
- 支持数组和对象的新增、删除属性
Proxy
能够直接监听对象的结构变化,无需通过额外方法(如Vue.set
)来处理新增或删除的属性。 - 按需监听
只有在属性被访问时才进行代理处理,大大减少了不必要的性能开销,提升了性能。
Vue 2 和 Vue 3 响应式的主要对比
特性 | Vue 2 (Object.defineProperty ) | Vue 3 (Proxy ) |
---|---|---|
实现方式 | Object.defineProperty | Proxy |
深层监听 | 需要递归遍历整个对象 | 自动支持,按需代理 |
数组操作 | 需要使用 Vue.set 或 Vue.delete 处理 | 自动支持新增和删除 |
性能 | 数据量大或层级深时性能较差 | 更高效,减少不必要的代理和递归 |
兼容性 | 支持 IE 11 | 不支持 IE 11 |
总结
Vue 2 使用 Object.defineProperty
实现响应式,在某些情况下需手动处理数组和对象的属性增删,且大规模数据下性能有限。而 Vue 3 通过 Proxy
提供了更完善的响应式系统,提升了性能并增强了开发体验,尤其在复杂数据结构的监听和高效性上表现更佳。
6. Proxy 只会代理对象的第⼀层,那么 Vue3 ⼜是怎样处理这个问题的呢?
虽然 Proxy
只能直接代理对象的第一层属性,但 Vue 3 通过按需递归代理(惰性代理)的机制,实现了深层次的响应式支持。以下是 Vue 3 如何处理这一问题的详细说明:
Vue 3 的按需递归代理机制
惰性代理
Vue 3 通过Proxy
代理对象的第一层属性。当访问到嵌套属性时(触发get
操作),Vue 会判断该属性是否已被代理。如果该嵌套属性是普通对象且未被代理,那么 Vue 会在此时为它创建一个新的Proxy
,使其成为响应式对象。这种方式即按需递归代理,避免了初始化时的深度递归遍历。依赖收集与响应式追踪
当代理对象的属性被访问时,Vue 的响应式系统会收集依赖,确保数据变动时能够触发相关更新。如果嵌套属性被代理后发生变化,Vue 会通过Proxy
的set
拦截器触发响应式更新,确保视图随嵌套数据变化而自动更新。
优点
- 性能优化
按需递归代理只在嵌套属性被访问时才创建代理,避免了初始化时对整个对象的深度遍历和代理处理,节省了性能开销。 - 深层嵌套支持
通过这种惰性代理,Vue 3 能够自动支持任意深层级的嵌套数据。只要被访问的嵌套属性会自动变为响应式,解决了Proxy
仅代理第一层的问题。
总结
Vue 3 通过按需递归代理的机制,使得响应式系统能够动态生成嵌套属性的代理对象,保证了深层数据的响应式能力,同时避免了性能开销。这种机制是 Proxy
与 Vue 3 响应式系统结合的核心优势所在。
7. 监测数组的时候可能触发多次 get/set,那么如何防⽌触发多次呢?
我们可以判断 key 是否为当前被代理对象 target ⾃身属性,也可以判断旧值与新值是否相等,只有满⾜以上两个条件之⼀时,才有可能执⾏ trigger。
8. Vue 2 与 Vue 3 区别对比表
特性 | Vue 2 | Vue 3 |
---|---|---|
响应式系统 | 使用 Object.defineProperty ,无法监听新增或删除的属性 | 使用 Proxy ,支持监听新增、删除属性及嵌套属性的变化 |
性能 | 性能较差,尤其在复杂数据和大量更新时 | 提升了性能,使用 Proxy 和优化的虚拟 DOM 渲染 |
API 风格 | 使用 Options API,通过 data 、methods 等定义组件逻辑 | 引入 Composition API,通过 setup 和 ref 、reactive 等更灵活地组织组件逻辑 |
TypeScript 支持 | 支持较差,需要额外的工具,如 vue-class-component 和 vue-property-decorator | 原生支持 TypeScript,类型推导更为完善 |
生命周期钩子 | 使用传统的生命周期钩子:created 、mounted 等 | 新增 setup 钩子,生命周期钩子通过 onMounted 、onUpdated 等在 setup 内使用 |
Fragment 支持 | 只能有一个根元素 | 支持 Fragment,一个组件可以有多个根元素 |
Teleport 和 Suspense | 不支持 | 支持 Teleport 和 Suspense,处理异步组件和跨位置渲染,defineAsyncComponent 新增异步组件 |
Tree Shaking | 不完全支持,可能会引入不必要的代码 | 更好的 Tree Shaking 支持,模块化拆分,减少打包大小 |
自定义渲染器 | 仅支持 DOM 渲染 | 支持自定义渲染器,可以在非 DOM 环境(如移动端)使用 Vue |
支持的浏览器 | 支持 IE 9+ | 不支持 IE 11,仅支持现代浏览器 |
其他:
cacheHandler
事件监听缓存hoistStatic
静态节点提升Fragments
Vue3 中不在要求模版的跟节点必须是只能有一个节点。跟节点和和 render 函数返回的可以是纯文字、数组、单个节点,如果是数组,会自动转化为 Fragments
9. 介绍一下 nextTick
vue 中数据是同步更新的,视图是异步更新。所有在我们同步代码中修改了数据,是无法访问更新后的 DOM。所以官方就提供了 nextTick。vue 中响应式数据改变,不会立即更新 DOM,而是更新了 vnode,等下一次事件循环才一次性去更新 DOM。
vue2 实现:判断 Promise、MutationObserver、setImmediate、setTimeout 兜底
vue3 实现:Promise.then
10. 怎么理解 Vue 的单向数据流?
单向数据流是指:数据在组件树中的流动方向,是从父组件流向子组件的。这个设计使得数据流更加可预测和易于调试,确保应用状态的一致性。
简单理解:父组件的状态对于子组件是只读的,子组件想改,只能通过事件的方式,通知父组件自己改
11. 组件通信有哪些?
通信方式 | 适用场景 | 优缺点 |
---|---|---|
Props (父 -> 子) | 父组件向子组件传递数据 | 简单直观,但不适合复杂的多层嵌套组件间通信 |
$emit (子 -> 父) | 子组件向父组件传递事件或数据 | 确保父子之间有清晰的层级结构和数据流 |
通过父组件(中介) | 兄弟组件之间通过父组件通信 | 适用于简单场景,但多层嵌套时较为复杂 |
Event Bus | 兄弟组件、跨层级组件通信(非父子) | 适合解耦,但在大型应用中可能导致代码混乱 |
Vuex | 跨多个组件共享全局状态 | 适用于大型应用,管理复杂状态,但需要更多的配置 |
Provide / Inject | 适用于祖先组件与后代组件的通信 | 简化了深层嵌套的通信,但可能增加组件耦合 |
全局事件总线 | 适用于需要跨越多个组件的事件传递 | 适用于简单场景,但不适合大型项目 |
也可以获取父子组件的实例,然后去访问数据
12. Vue 中怎么异步加载组件
<template>
<MyComponent />
</template>
<script setup>
import { defineAsyncComponent } from "vue";
const MyComponent = defineAsyncComponent(() =>
import("./components/MyComponent.vue")
);
</script>
13. 父组件如何监听子组件生命周期?
- vue2 使用 @hook:mounted
- vue3 使用 @vue:mounted
- 自定义事件,在子组件生命周期中去执行 下面是 vue3 的写法
<template>
<h1 @click="send">Home 页面</h1>
<Text @vue:mounted="fn" />
</template>
<script setup>
import { onMounted, ref } from "vue";
import Text from "../components/Text.vue";
const fn = () => {
console.log("Text mounted");
};
</script>
14. 说一下 watch 和 computed 的区别
在 Vue 中,watch
和 computed
都是响应式机制的一部分,主要用于监听和计算数据的变化。然而,它们的应用场景和工作方式有所不同。
1. computed
的特性
定义:
computed
用于定义基于响应式数据的计算属性。它会根据其依赖的数据变化而重新计算,并且计算结果会被缓存,直到依赖的数据变化为止。
特点:
- 缓存:计算属性的值会缓存,只有当它所依赖的响应式数据变化时,才会重新计算。
- 同步:
computed
计算属性的求值是同步的。 - 性能优化:当多个地方依赖于同一个计算属性时,
computed
会避免重复计算,只有依赖的数据变化时才会重新计算。
使用场景:
- 适合用于计算和返回基于响应式数据的衍生数据。
2. watch
的特性
定义:
watch
用于观察和响应数据的变化。当某个数据发生变化时,watch
会执行相应的回调函数。watch
适用于执行异步操作或进行副作用(如数据更新、API 调用等)时。
特点:
- 不缓存:
watch
监听的是某个响应式数据的变化,每次变化都会触发回调函数,且回调是异步执行的。 - 副作用:
watch
适用于处理副作用操作,如发起网络请求、操作外部 API 等。 - 异步:
watch
的回调是异步的,可以用于处理需要执行副作用的操作。
使用场景:
- 适合用于执行副作用、异步任务或者某些数据变化后的操作。
3. computed
和 watch
的对比
特性 | computed | watch |
---|---|---|
主要用途 | 用于计算衍生状态,基于已有数据计算返回值 | 用于监听数据变化,处理副作用或异步操作 |
缓存 | 会缓存,只有依赖的响应式数据变化时才会重新计算 | 不缓存,每次数据变化都会触发回调 |
计算类型 | 同步计算,结果会立即返回 | 异步执行,适合需要执行副作用的任务 |
使用场景 | 适用于计算属性(如合成数据、格式化等) | 适用于观察数据变化并执行副作用(如异步请求) |
触发时机 | 当计算属性的依赖数据变化时自动重新计算 | 当监听的响应式数据发生变化时执行回调 |
总结
computed
:适用于基于现有数据计算衍生数据,具有缓存特性,不会重复计算,适合需要性能优化的场景。watch
:适用于监听数据变化并执行副作用(如异步操作、API 请求等),可以用于处理复杂的逻辑或与外部环境交互。
根据实际需求选择合适的方式,computed
用于计算,watch
用于观察和执行操作。
15. Vue 3 中 ref
、toRef
、toRefs
和 reactive
的总结
在 Vue 3 的 Composition API 中,ref
、toRef
、toRefs
和 reactive
都用于创建和管理响应式数据。它们的主要区别在于使用场景、返回值类型以及是否支持深度响应式。
总结对比
特性 | ref | reactive | toRef | toRefs |
---|---|---|---|---|
适用类型 | 基本数据类型、对象、数组等 | 对象、数组等复杂数据 | 响应式对象中的单个属性 | 将整个 reactive 对象的属性转换为 ref |
返回值类型 | ref 对象 | 响应式对象 | 返回 ref | 返回对象,属性为 ref |
深度响应式 | 不会深度代理(基本类型数据) | 深度代理整个对象 | 仅代理指定属性 | 代理对象中所有属性 |
使用场景 | 创建简单响应式变量 | 对象或数组的深度响应式管理 | 将对象中的属性转为单独的 ref | 批量将 reactive 对象的属性转换为 ref |
ref
:用于创建基本数据类型的响应式变量,或将对象、数组等包装为响应式引用。reactive
:用于创建整个对象或数组的深度响应式,适合管理复杂的数据结构。toRef
:将reactive
对象的某个属性转换为单独的ref
,保持原对象的响应式。toRefs
:将reactive
对象的所有属性转换为ref
,适合批量操作和解构对象。
根据数据类型和需求的不同,选择合适的方式来管理响应式数据。
16. Vue 中怎么做全局错误监听?
// main.js 入口文件
const app = createApp(App);
app.config.errorHandler = (err, vm, info) => {
console.log(err, vm, info);
// 错误日志上报
};
window.onerror = function (event) {};
// 未处理的promise错误
window.onunhandledrejection = function (event) {};
17. Vue3 中怎么访问实例?
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const appContext = instance.appContext;
18. 怎么监听子组件内的错误?
// 子组件
throw new Error("Error");
//父组件
import { onErrorCaptured } from "vue";
// 监听到子组件错误,执行回调
onErrorCaptured((err) => {
console.log("error", err);
});
19. keep-alive
是如何实现的
keep-alive
是 Vue 提供的一个内置组件,用于缓存不活动的组件,以提高应用性能。它可以在组件切换时保持组件的状态,而不是每次切换时重新创建和销毁组件。
1. 实现原理
组件缓存机制
keep-alive
的核心思想是当一个组件被切换出视图时,并不会被销毁,而是被保留在内存中。只有当该组件再次被需要时,keep-alive
会将其从缓存中恢复,而不是重新创建。
使用 include
和 exclude
控制缓存
keep-alive
可以通过include
和exclude
属性来指定哪些组件需要缓存,哪些组件不需要缓存。include
:一个字符串、正则表达式或数组,指定哪些组件应被缓存。exclude
:一个字符串、正则表达式或数组,指定哪些组件不应被缓存。
生命周期钩子
- 在
keep-alive
缓存的组件中,会触发一些特定的生命周期钩子:activated
:当组件从缓存中恢复并显示时,触发此生命周期钩子。deactivated
:当组件被缓存(即切换出视图)时,触发此生命周期钩子。
如何工作
- 当一个组件包裹在
keep-alive
中并第一次渲染时,组件会正常初始化并挂载。 - 当切换离开这个组件时,
keep-alive
会将该组件缓存起来,而不是销毁它。 - 当再次切换回这个组件时,
keep-alive
会从缓存中恢复该组件的状态,而不是重新创建组件实例。
2. 实现方式
Vue 通过对组件的 created
、mounted
、destroyed
等生命周期钩子的处理,配合 vnode
(虚拟节点)缓存机制来实现组件缓存。
- 缓存组件实例:当组件被包裹在
keep-alive
中时,Vue 会在内部维护一个缓存池。每个组件实例都会根据其key
值被存储。 - 复用实例:当组件需要被复用时,Vue 会直接从缓存池中取出实例,而不是重新创建一个新的实例。
- 组件销毁与恢复:在
keep-alive
内部,只有组件被切换离开并且不再活跃时,它才会被缓存。再次显示该组件时,会触发其activated
钩子,恢复之前的状态。
3. 常见使用场景
- Tab 页面切换:当用户在多个 Tab 之间切换时,可以通过
keep-alive
缓存已经访问过的 Tab 页面,避免每次切换时重新加载数据。 - 表单填写:对于较长的表单填写,可以保持表单的输入状态,避免用户每次返回时丢失已填内容。
优先级规则
在 keep-alive
中,exclude
的优先级高于 include
。
4. 总结
- 缓存机制:
keep-alive
通过缓存组件实例来避免重复的创建和销毁,提升性能。 - 生命周期钩子:
activated
和deactivated
允许开发者在组件恢复和离开时执行特定的操作。 - 控制缓存:
include
和exclude
提供了灵活的缓存控制。
keep-alive
是 Vue 提供的优化性能的利器,特别适用于需要频繁切换的组件,通过减少不必要的组件销毁和重建,大大提升用户体验。
20. vue-router 怎么动态添加、删除路由?
import { useRouter } from "vue-router";
const router = useRouter();
const addRoute = () => {
const newRoute = {
path: "/hello",
name: "hello",
component: () => import("../components/HelloCarlos.vue"), // 动态加载组件
};
router.addRoute(newRoute); // 添加路由
};
router.removeRoute("xxx"); // 移除路由
21. 页面刷新 vuex 数据丢失怎么处理?
使用第三方插件 进行 持久化数据 如
vuex-persistedstate
写插件
注册时取、离开时存
- vuex 注册时,从缓存取数据 进行 merge
- 监听页面刷新状态 页面离开之前 存数据
- 使用 localstorage、sessionstorage 进行存储
22. SPA 怎么优化首页白屏时间?
最终解决方案:SSR 服务端渲染
SPA: Single Page Application(单页应用),Vue 和 React 都是。 Vue 项目中,我们打完包生成 dist
时,如果没有指定多入口,就只会生成一个 html
文件,和一些 js、css、图片等资源文件。html
中并没有渲染元素,就一个 id 为 app 的 div。页面上的元素都是通过请求 js
动态渲染上去。所有本身就挺慢的。
- 路由懒加载,首屏只请求首页相关
js
和css
- 骨架屏优化交互体验
- 组件异步加载,首页先渲染首屏的资源,首屏下面的组件异步加载
- 尽量减少 http 请求,因为浏览器有最大并发数限制,可以利用 http2 多路复用
- 利用打包工具做代码拆分,
splitChunks
- 代码体积压缩
gzip
,打印日志去除 - 图片压缩、图片存入
CDN
23. v-memo 是干嘛用的?
v-memo
是 Vue 3.3 引入的一个优化渲染性能的指令。它允许开发者缓存组件或元素的渲染结果,只有当依赖的数据发生变化时,才会重新渲染。这有助于减少不必要的渲染,提升应用的性能,特别是对于复杂计算结果或不常变化的数据。
主要功能
- 缓存渲染结果:
v-memo
会缓存组件或元素的渲染结果,直到指定的依赖发生变化时,才会触发重新渲染。 - 减少不必要的渲染:当渲染依赖的数据没有变化时,
v-memo
会使用缓存的结果,从而避免重复渲染,优化性能。
使用场景
- 复杂计算的优化:当组件渲染依赖于复杂的计算时,如果这些计算结果不常变化,使用
v-memo
可以避免每次渲染时都进行相同的计算。 - 不常变化的数据:对于不经常变化的响应式数据,
v-memo
可以减少视图的更新频率,从而提高性能。
工作原理
v-memo
会将依赖项(如响应式数据或计算属性)传入,并基于这些依赖判断是否需要重新渲染。如果依赖没有变化,v-memo
会缓存当前的渲染结果,而不是重新渲染视图。
<template>
<div v-memo="[dependency1, dependency2]">
<!-- 这里的内容会被缓存,直到依赖项变化 -->
</div>
</template>
总结
- 性能优化:
v-memo
提供了减少不必要渲染的能力,尤其在复杂视图或频繁变化的数据场景中,可以显著提高性能。 - 依赖控制:通过指定依赖项,开发者可以精确控制何时重新渲染视图,避免过度渲染带来的性能损失。
24. 说一下 Vue3 声明一个响应式数据的方式?
ref
: 通过.value 访问及修改reactive
: 直接访问、只能声明引用数据类型computed
: 也是通过.value,声明需要 传 get、settoRef
: 类似 ref 的用法,可以把响应式数据的属性变成 reftoRefs
: 可以把响应式数据所有属性 转成一个个 refshallRef
: 浅层的 ref,第二层就不会触发响应式shallReactive
: 浅层的 reactive,第二层就不会触发响应式customRef
: 自定义 ref
25.说一下 watch 和 watchEffect?
特性 | watch | watchEffect |
---|---|---|
目标 | 监听一个或多个指定的数据源变化 | 自动追踪所有响应式数据的变化 |
需要传入数据源 | 需要明确指定监听的响应式数据或计算属性 | 无需传入数据源,会自动追踪作用域中的所有依赖 |
依赖收集 | 手动指定依赖 | 自动收集依赖,回调会自动执行 |
回调函数参数 | 可访问 oldValue 和 newValue | 不支持 oldValue 和 newValue ,只会执行副作用 |
执行时机 | 默认异步执行,回调会被推入微任务队列 | 同步执行,立即触发副作用 |
总结
watch
:当你需要精确控制何时响应数据变化,并且需要访问数据变化前后的值时,使用watch
。适用于精细化的副作用管理,尤其是对单一数据源的观察,默认不立即执行,可以配置immediate
默认不深度监听,可以配置deep
。watchEffect
:当你希望自动追踪所有响应式数据的变化,并且立即执行副作用时,使用watchEffect
。它适用于副作用函数需要自动依赖收集,并且在数据变化时同步执行副作用。
选择使用哪一个取决于你的需求,watch
提供更多的控制和灵活性,而 watchEffect
更简洁和自动化。
26. 说一下 Vue3 中的宏有哪些?
defineProps
: 声明 props defineEmits
: 声明 emit defineModel
: 用来声明一个双向绑定 prop defineExpose
: 指定对外暴露组件的属性 defineOptions
:在 script setup 中提供组组件属性 defineSlots
: 声明 slots defineComponent
: 声明定义组件
27. 说一下 vue2.x 中如何监测数组变化
使用了函数劫持的方式,重写了数组的方法,Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
28. 组件中的 data 为什么是一个函数?
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数
。如果 data 是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间 data 不冲突,data 必须是一个函数。
29. Vue 模板编译原理
Vue 的模板编译原理是其核心机制之一,它将开发者编写的模板(HTML 结构和指令)转换成 JavaScript 渲染函数,从而动态生成 DOM。Vue 模板的编译过程主要分为以下几个阶段:解析、优化、生成。
1. 编译过程概述
Vue 模板编译的过程是一个从模板字符串到渲染函数的转化过程。这个过程会将模板中的指令(如 v-if
、v-for
)和数据绑定(如 )转换成 JavaScript 渲染函数,最终生成的渲染函数用于创建和更新虚拟 DOM。
Vue 模板编译过程主要分为三个阶段:
- 解析阶段:将模板字符串解析成抽象语法树(AST)。
- 优化阶段:对 AST 进行优化,标记静态节点。
- 代码生成阶段:根据优化后的 AST 生成渲染函数。
30.Vue2.x 和 Vue3.x 渲染器的 diff 算法分别简单的说一下
简单来说,diff 算法有以下过程
- 同级比较,再比较子节点
- 先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
- 比较都有子节点的情况(核心 diff)
- 递归比较子节点
正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从 O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x 借鉴了ivi
算法和 inferno
算法
在创建VNode
时就确定其类型,以及在mount/patch
的过程中采用位运算来判断一个VNode
的类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提升。
该算法中还运用了动态规划
的思想求解最长递归子序列
。
Vue2.x 和 Vue3.x Diff 算法的主要区别
特性 | Vue 2.x | Vue 3.x |
---|---|---|
虚拟 DOM 结构 | 基于对象的树结构 | 支持 Fragment,更加灵活的虚拟 DOM 结构 |
Diff 算法 | 双端比较,通过两个指针从头尾比较 | 通过 key 进行优化,减少无效的 DOM 更新 |
静态节点优化 | 静态节点优化,静态节点只渲染一次 | 静态节点和子树优化,避免不必要的重新渲染 |
性能优化 | 基于双端比较的算法,可能存在性能瓶颈 | 更精细的 Proxy 代理和响应式优化 |
动态组件的处理 | 需要根据 key 进行手动优化 | 更高效的动态组件处理,利用 key 优化渲染 |
渲染层级 | 对同层级节点进行双端比较 | 更精确的层级更新,避免不必要的节点销毁 |
复杂场景性能瓶颈 | 较大节点树和频繁更新时性能下降 | 在大多数场景下性能更优,但依然可能遇到瓶颈 |
总结
- Vue 2.x 的 Diff 算法相对简单,基于双端比较策略,但在复杂的更新场景下,可能会存在性能瓶颈,尤其是在大量节点和频繁更新的情况下。
- Vue 3.x 通过引入更高效的 Diff 算法、响应式系统、Fragment 支持以及精细化的更新策略,显著提高了性能,特别是在大规模应用或复杂场景下,能够更加智能地进行虚拟 DOM 的更新和渲染。
Vue 3.x 相较于 Vue 2.x,改进了 Diff 算法的效率,使得 Vue 在大规模应用中具备了更好的响应性和性能表现。
31. Vue 路由的实现原理与模式
Vue 路由(Vue Router)是 Vue.js 官方的路由管理库,它为单页应用(SPA)提供了前端路由功能。Vue 路由的核心原理和模式有助于在应用中管理不同视图之间的切换。
1. Vue 路由实现原理
Vue 路由的核心思想是通过 URL 路径变化 来改变视图,进而动态地加载和切换不同的组件。路由的实现依赖于 JavaScript 的 History API 和 Hash API,以及 Vue 组件系统来渲染相应的视图。
1.1 路由的配置
Vue 路由的基本实现原理是通过一个 routes
数组来配置不同的路由和组件映射关系。在 Vue 实例中配置路由,并通过 router-view
来渲染路由对应的组件。
1.2 路由匹配
当用户访问一个 URL 时,Vue 路由会根据 URL 路径与配置的路由规则进行匹配,找到与之对应的组件进行渲染。Vue Router 使用 路径匹配 来查找对应的路由规则。
- 如果路由是静态的,它会根据路径来查找对应的组件。
- 如果是动态路由(如
/user/:id
),它会解析参数并传递给组件。
1.3 组件渲染
<router-view>
是 Vue 中渲染路由组件的占位符,它会根据当前的路由信息来显示不同的组件。- 当路径变化时,Vue Router 会重新渲染
<router-view>
,根据新的路由路径显示相应的组件。
1.4 路由历史管理
Vue 路由通过 pushState
或 hash
来管理历史记录,从而实现路径的变化和视图的切换。Vue Router 会在浏览器的历史记录中加入新的记录,并且允许在路由跳转时不会重新加载页面。
1.5 导航守卫
Vue 路由还支持导航守卫,用于在路由变化时进行钩子操作(如权限校验、数据加载等)。常见的导航守卫有:
- 全局守卫:
beforeEach
、afterEach
- 路由独享守卫:
beforeEnter
- 组件内守卫:
beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
1.6 懒加载与动态路由
Vue Router 支持懒加载和动态路由组件。通过 route-level code-splitting
,路由组件可以按需加载,减少初始加载时的资源体积。
2. Vue 路由模式
Vue Router 支持多种路由模式,主要有以下几种:
2.1 Hash 模式
- 原理:Hash 模式利用 URL 中的
#
符号来标识路由路径,改变#
后的路径不会导致页面重新加载,而是触发路由匹配。 用hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染) - 优点:
- 不需要服务器配置,能兼容所有浏览器。
- 路由的变化不会导致页面刷新。
- 缺点:
- URL 中会带有
#
,不够美观。 - 不支持服务端渲染时的 SEO 优化。
- URL 中会带有
工作原理:
- 访问的 URL 会是
http://example.com/#/home
,浏览器会监听#
后的变化来触发路由更新。
2.2 History 模式
pushState
和 repalceState
两个 API 来操作实现 URL 的变化, 我们可以使用 popstate
事件来监听 url 的变化,从而对页面进行跳转(渲染), history.pushState()
或 history.replaceState()
不会触发 popstate
事件,这时我们需要手动触发页面跳转(渲染)。
popstate
事件的触发时机
触发条件:
popstate
事件会在浏览器的历史记录发生变化时触发,通常发生在以下情况:- 用户点击浏览器的“后退”或“前进”按钮。
- 使用
history.back()
、history.forward()
或history.go()
等方法进行导航。
注意:
pushState()
和replaceState()
只是改变历史记录,而不会直接触发popstate
事件。只有当浏览器的历史记录栈发生变化时(例如用户进行前进/后退操作),popstate
事件才会被触发。原理:History 模式使用 HTML5 的
history.pushState
和history.replaceState
方法来改变浏览器的历史记录,并且不再使用#
符号。这使得 URL 更加干净且更符合传统的路径结构。优点:
- URL 结构清晰,没有
#
符号,符合传统 URL 的风格。 - 支持前后端分离开发,更好地与后端路由匹配。
- URL 结构清晰,没有
缺点:
- 需要服务器配置。如果直接访问某个路由路径(如
/user
),服务器需要返回整个前端应用,否则会出现 404 错误。
- 需要服务器配置。如果直接访问某个路由路径(如
工作原理:
- 访问的 URL 是
http://example.com/home
,浏览器通过pushState
改变历史记录,路径变化时 Vue 路由会监听并更新视图。
2.3 Abstract 模式
- 原理:Abstract 模式是 Vue Router 的一种内部模式,它不依赖浏览器的 URL,而是通过 JavaScript 内部的历史记录进行路径变化。在 Node.js 环境下,常用这种模式,适合服务端渲染或在测试中使用。
- 优点:
- 不依赖浏览器 URL,适用于非浏览器环境,如服务端渲染。
- 缺点:
- 不适用于常规的浏览器应用,因为它不处理真实的 URL。
2.4 使用场景对比
路由模式 | 适用场景 | 优缺点 |
---|---|---|
Hash 模式 | 适合小型应用和不需要服务器配置的应用 | 优:简单,不需要服务器配置;缺:URL 不美观,SEO 差 |
History 模式 | 适合大部分现代浏览器环境,前后端分离应用 | 优:干净的 URL,支持与服务器的路由一致;缺:需要服务器配置 |
Abstract 模式 | 适合服务端渲染或测试环境 | 优:不依赖浏览器,适合服务端;缺:无法直接在浏览器中使用 |
3. 总结
- Hash 模式:通过 URL 中的
#
来实现路径变化,适合不需要服务器配置的情况。 - History 模式:通过
history.pushState
来实现路径变化,适合前后端分离的 SPA 应用,但需要服务器支持。 - Abstract 模式:适用于服务端渲染或 Node.js 环境,不依赖 URL,适合在非浏览器环境使用。
Vue 路由的工作原理通过 Hash
或 History
等模式管理 URL,并通过虚拟 DOM 更新视图,结合 Vue 的组件机制,动态地加载视图,实现 SPA(单页应用)的流畅体验。
32.说一下 v-if 和 v-show 的区别
当条件不成立时,v-if 不会渲染 DOM 元素,v-show 操作的是样式(display),切换当前 DOM 的显示和隐藏。
vue2 v-for 中优先级高 ,vue3 中 v-for 的优先级高
33. Vuex 与 Pinia 的对比与差异
Vuex 和 Pinia 都是用于 Vue.js 中的状态管理工具,Vuex 是 Vue 2.x 时代的官方库,而 Pinia 是 Vue 3.x 推荐的状态管理库。它们各有优缺点,下面是两者的对比。
1. 基本概念与设计理念
特性 | Vuex | Pinia |
---|---|---|
设计理念 | Vuex 是一个完整的状态管理框架,适合中到大型应用 | Pinia 是轻量级状态管理库,专为 Vue 3 设计 |
数据存储 | 使用 store 和 modules 来管理状态 | 使用 store,支持模块化,但更简单直观 |
依赖 | 依赖 Vue.js 和 Vue Router | 仅依赖 Vue 3,轻量级,不再需要 Vue Router |
语法风格 | 基于 Vuex 的传统 API,使用 mutations、actions 等 | 更简洁现代,使用 defineStore,API 更直观 |
2. API 与使用方式
特性 | Vuex | Pinia |
---|---|---|
创建 store | new Vuex.Store({...}) | defineStore({...}) |
存储方式 | 使用 state 、getters 、mutations 、actions | 使用 state 、getters 、actions (支持模块化) |
类型支持 | 需要手动配置类型支持,Vuex 3.x 不完全支持 TS | 内建支持 TypeScript,自动推导类型 |
模块化 | 支持 store 模块化 | 更简洁,模块化非常轻量,直接支持复用 |
3. 开发体验与功能特性
特性 | Vuex | Pinia |
---|---|---|
类型推导支持 | 需要手动配置,类型支持较弱 | 强大的类型推导,使用 TypeScript 更加便捷 |
热重载 | 热重载支持较差(需要插件支持) | 原生支持热重载,开发体验更好 |
插件支持 | 支持各种插件,如 vuex-persistedstate | 轻量,内置支持 DevTools,可扩展插件支持 |
DevTools 支持 | 支持 Vuex DevTools,但较为复杂 | 原生集成 Vue DevTools,操作更为简便 |
4. 性能与优化
特性 | Vuex | Pinia |
---|---|---|
性能 | 虽然性能优秀,但存在较高的内部复杂性 | 更轻量,优化了响应性和性能 |
响应性 | 使用 Vue.set 和 mutations 处理响应性 | 基于 Vue 3 的 Proxy,响应性更高效 |
模块化管理 | Vuex 支持复杂的模块化,适合大型项目 | 更简单直观的模块化管理,避免过多的复杂配置 |
5. 学习曲线
特性 | Vuex | Pinia |
---|---|---|
学习曲线 | 比较陡峭,需要了解 Vuex 的基础概念(state、mutations、actions) | 学习曲线较平缓,API 设计简单,特别适合新手 |
文档 | 完善,但学习时需要花费时间理解 Vuex 的概念和实现 | 更简洁的文档,易于上手,配合 Vue 3 使用更直观 |
6. 总结
特性 | Vuex | Pinia |
---|---|---|
推荐使用场景 | 适合中到大型应用,需要复杂的状态管理和插件支持 | 更适合 Vue 3 项目,特别是需要轻量、现代化状态管理时 |
灵活性 | 提供完整的状态管理功能,但较为复杂 | 更加简洁,灵活,符合 Vue 3 的设计理念 |
TypeScript 支持 | 支持,但配置较复杂 | 内建支持 TypeScript,类型推导更加智能 |
结论
- Vuex 是为 Vue 2.x 和复杂应用设计的,它适合需要高度自定义和多个模块的状态管理,但相对来说学习曲线较陡峭,开发过程较为繁琐。
- Pinia 是 Vue 3 专为现代化应用设计的状态管理工具,它更加轻量、易用,支持 TypeScript,并且与 Vue 3 的 Composition API 更加契合,适合中小型应用和开发者快速上手。
对于新项目,特别是 Vue 3 项目,推荐使用 Pinia,它提供了更现代化的开发体验。
34. Vue 与 React 有什么区别
不同点:
- 模版语法不同,react 采用 JSX 语法,vue 使用基于 HTML 的模版语法
- 数据绑定不同,vue 使用双向数据绑定,react 则需要手动控制组件的状态和属性。
- 状态管理不同,vue 使用 vuex 状态管理,react 使用 redux 状态管理
- 组件通信不同,vue 使用 props 和事件的方式进行父子组件通信,react 则通过 props 和回调函数的方式进行通信。
- 生命周期不同,vue 有 8 个生命周期钩子,react 有 10 个
- 响应式原理不同,vue 使用双向绑定来实现数据更新,react 则通过单向数据流来实现
相同点:
- 组件化开发:Vue 和 React 都采用了组件化开发的方式,将用户界面划分为独立、可复用的组件,从而使代码更加模块化、可维护和可扩展。
- 虚拟 DOM:Vue 和 React 都使用虚拟 DOM 技术,通过在 JavaScript 和真实 DOM 之间建立一个轻量级的虚拟 DOM 层,实现高效的 DOM 更新和渲染。
- 响应式更新:Vue 和 React 都支持响应式更新,即当数据发生变化时,会自动更新相关的组件和视图,以保持用户界面的同步性。
- 集成能力:Vue 和 React 都具有良好的集成能力,可以与其他库和框架进行整合,例如 Vue 可以与 Vuex、Vue Router 等配套使用,React 可以与 Redux、React Router 等配套使用。
35. 实现一个简单的 hook
import { ref, onMounted } from "vue";
/**
* useFetch Hook
* 用于从指定的 API 地址加载数据
* @param {string} url - 要请求的数据接口
* @returns {object} - 包含响应数据、加载状态、错误信息等的响应式数据
*/
export function useFetch(url) {
const data = ref(null); // 存储返回的数据
const error = ref(null); // 存储错误信息
const loading = ref(true); // 加载状态
// 当组件挂载时,发起数据请求
onMounted(async () => {
try {
const response = await fetch(url); // 发起 HTTP 请求
if (!response.ok) {
// 检查响应是否正常
throw new Error("Network response was not ok");
}
data.value = await response.json(); // 解析返回的数据
} catch (err) {
error.value = err.message; // 捕获并存储错误信息
} finally {
loading.value = false; // 数据加载完成,更新加载状态
}
});
return { data, error, loading };
}
36. Vue 3 中的 setup
函数
在 Vue 3 中,setup
是一个新的生命周期钩子函数,是 Vue 3 Composition API 的核心部分。它用于在组件实例创建之前进行逻辑的组织和初始化。通过 setup
,我们可以在函数内部访问组件的状态、计算属性、生命周期钩子等,完全替代了 Vue 2 中的 data
、methods
、computed
等选项式 API.
1. setup
函数的基本用法
setup
函数在组件创建时被调用,接收两个参数:props
和 context
。
import { ref, reactive, computed } from "vue";
export default {
setup(props, context) {
// `props` 接收父组件传递的 props
const count = ref(0); // 响应式的状态
// `context` 提供了一些额外的信息,如 `attrs`, `slots`, `emit`
const increment = () => {
count.value++;
};
// 计算属性
const doubledCount = computed(() => count.value * 2);
return {
count,
increment,
doubledCount,
};
},
};
37. Vue 3 中 v-model
的实现原理与使用场景
1. v-model
的基本概念
v-model
用于将组件的输入与父组件的状态进行双向绑定。它会自动将组件的 value
(或自定义的 prop)与父组件的某个属性进行同步,同时还会触发一个 input
事件(或自定义的事件)。
2. v-model
在 Vue 3 中的工作原理
2.1 默认绑定 modelValue
和 update:modelValue
事件
- Prop:Vue 3 将
v-model
绑定的属性名改为了modelValue
,它会传递给子组件。 - 事件:当子组件修改数据时,会触发一个
update:modelValue
事件,将新的值传递给父组件,完成双向数据绑定。
2.2 自定义 v-model
名称
- Vue 3 允许自定义
v-model
的 prop 名称和事件名称。在需要时,可以通过modelValue
的替代方式进行灵活配置。
2.3 通过 v-model
实现双向绑定
- 在父组件中,
v-model
会绑定到某个属性,且会监听update:modelValue
事件并更新该属性的值。
3. Vue 3 v-model
的改进
3.1 多 v-model
支持
- Vue 3 支持一个组件上多个
v-model
,允许为不同的 prop 分别绑定v-model
,提高了灵活性。
3.2 使用 v-model
与 emits
配合
- 在子组件中,
v-model
会自动与emits
配合,确保在事件触发时,父组件能接收到正确的值。
4. v-model
的使用场景
4.1 表单组件
v-model
是用于表单组件的经典用法,确保父组件可以与子组件中的输入数据进行双向绑定。
4.2 自定义组件的双向绑定
- 通过
v-model
,可以将父组件的状态与子组件的数据实现双向绑定,尤其适用于复杂的表单组件和表单元素。
4.3 状态同步
- 在父子组件之间,需要通过
v-model
实现状态同步时,使用该语法可以简化代码,减少手动的事件处理和 prop 传递。
5. 总结
- 原理:Vue 3 中的
v-model
默认通过modelValue
prop 和update:modelValue
事件实现双向绑定,支持自定义 prop 和事件名称。 - 优点:
- 简化双向绑定的语法。
- 支持多个
v-model
,允许为不同的 prop 绑定数据。 - 更灵活的自定义名称,提高了代码的可读性。
- 常见使用场景:用于表单输入组件、状态同步、复杂组件的双向绑定等。
v-model
使得在 Vue 中实现组件间的数据双向绑定变得更加简洁和灵活,极大提升了开发体验和代码维护性。
38. Vue 3 支持动态组件的实现方式
Vue 3 支持动态组件的功能,允许根据条件动态切换组件,提升了组件的灵活性和可复用性。动态组件通过 component
标签与 v-bind
结合使用,可以在运行时根据不同的条件渲染不同的组件。
1. 动态组件的基本语法
1.1 使用 component
标签
在 Vue 3 中,动态组件的基本实现通过 component
标签来完成。component
标签的 is
属性可以绑定一个动态值,这个值可以是一个组件的名称、组件对象或者通过 v-bind
动态绑定的值。
<template>
<component :is="currentComponent" />
</template>
<script>
import { ref } from "vue";
import ComponentA from "./ComponentA.vue";
import ComponentB from "./ComponentB.vue";
export default {
setup() {
const currentComponent = ref(ComponentA); // 默认渲染 ComponentA
// 通过动态切换 currentComponent 的值来渲染不同的组件
setTimeout(() => {
currentComponent.value = ComponentB; // 切换为 ComponentB
}, 2000);
return { currentComponent };
},
};
</script>
1.2 v-bind 和 is 属性结合使用
动态组件可以通过 v-bind
传递额外的属性给组件。在动态渲染组件时,使用 v-bind
可以将 props
、事件等动态传递给被渲染的组件。
<template>
<component :is="currentComponent" v-bind="componentProps" />
</template>
<script>
import { ref } from "vue";
import SomeComponent from "./SomeComponent.vue";
export default {
setup() {
const currentComponent = ref(SomeComponent);
const componentProps = ref({
title: "Dynamic Component",
description: "This is a dynamically rendered component.",
});
return { currentComponent, componentProps };
},
};
</script>
39. 插槽的概念与使用
插槽是 Vue 组件的一个重要特性,它使得父组件可以将内容传递到子组件中,并且允许子组件在模板中指定插槽的插入位置。Vue 3 对插槽进行了改进,增强了其灵活性和功能,支持更多复杂的场景和使用方式。
1. 插槽的基本概念
插槽允许父组件将内容传递给子组件,并且子组件可以在其模板中的特定位置展示这些内容。插槽使得组件更加灵活,特别是在需要将组件内容动态传递给子组件时。
2. Vue 3 插槽的基本使用
2.1 默认插槽
默认插槽用于渲染没有指定名称的插槽内容。子组件通过 <slot></slot>
标签渲染父组件传递的内容。
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用默认插槽传递内容:
<template>
<ChildComponent>
<p>This is default slot content</p>
</ChildComponent>
</template>
2.2 命名插槽
命名插槽使得父组件可以选择性地将内容传递给子组件的不同插槽。通过 slot 属性为插槽指定名称。
子组件定义命名插槽:
<template>
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
</div>
</template>
父组件传递命名插槽的内容:
<template>
<ChildComponent>
<template #header>
<h1>Header Content</h1>
</template>
<template #footer>
<footer>Footer Content</footer>
</template>
</ChildComponent>
</template>
2. Vue 3 作用域插槽(Scoped Slots)
作用域插槽允许父组件访问子组件传递的动态数据。父组件不仅可以传递内容,还可以通过插槽的作用域接收来自子组件的数据。
子组件通过 slot 标签并使用 v-bind 将数据传递给父组件。
<template>
<div>
<slot :message="message"></slot>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello from Scoped Slot",
};
},
};
</script>
父组件通过插槽的作用域接收数据,并使用数据:
<template>
<ChildComponent>
<template #default="{ message }">
<div>{{ message }}</div>
</template>
</ChildComponent>
</template>
40.Vue $route与$router 分别是干什么?
1. $route
- 功能:用于访问当前路由的信息,如路径、参数、查询字符串等。
- 用途:可以用来获取当前路由的详细信息,例如当前的路径、动态路由参数、查询字符串等。它是一个只读对象,不允许直接修改。
主要属性:
path
:当前路由的路径。params
:动态路由的参数(如通过/user/:id
定义的参数)。query
:URL 中的查询参数(如?id=123
)。hash
:URL 中的哈希值(如#section1
)。fullPath
:完整的 URL(包含路径、查询、哈希)。name
:当前路由的名字(如果定义了路由名称)。meta
:路由的元信息,通常用于路由的权限、标题等。
2. $router
- 功能:用于控制路由的跳转和导航,提供路由操作的 API。
- 用途:可以用来编程式地进行路由跳转、导航等操作。
主要方法:
push(location)
:将一个新的路由推入到历史记录栈,触发路由跳转。replace(location)
:替换当前的路由记录,跳转到新的路由,不会在历史记录中留下当前记录。go(n)
:向前或向后移动 n 步,等同于浏览器的history.go()
。back()
:回到历史记录中的上一个路由,等同于浏览器的history.back()
。forward()
:前进到历史记录中的下一个路由,等同于浏览器的history.forward()
。
总结
对象 | 功能 | 使用场景 |
---|---|---|
$route | 获取当前路由信息 | 读取路由参数、路径、查询、哈希等 |
$router | 路由跳转和控制 | 实现编程式导航、跳转到其他路由、回退等 |
41. v-model 是否破坏了 Vue 的单项数据流?
v-model
并不会破坏 Vue 的单项数据流。Vue 的单项数据流指的是:父组件通过 props
将数据传递给子组件,而子组件通过事件将数据的变化通知父组件。v-model
只是这种数据流模式的语法糖,并没有改变数据流动的方向。
v-model
如何保持单项数据流?
在 Vue 3 中,v-model
的实现机制基于 props
和 events
的双向绑定,具体通过以下两个步骤来实现父子组件的数据同步:
父组件传递数据到子组件:父组件通过
v-model
绑定,将数据传递给子组件。子组件通过modelValue
属性接收该数据。子组件通过事件通知父组件更新数据:当子组件中的数据发生变化时,通过触发
update:modelValue
事件,将更新的数据传递回父组件。父组件接收到这个事件后,更新自身的数据,再次通过props
将更新后的数据传递给子组件。
v-model
对单项数据流的影响
v-model
依然遵循 Vue 的单项数据流设计:
- 数据流动方向:数据始终是从父组件传递到子组件。
- 更新流动方向:数据更新是通过事件的方式从子组件传递到父组件,再由父组件更新数据并传递回子组件。
父组件
<!-- 父组件 -->
<template>
<ChildComponent v-model="parentData" />
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent.vue";
const parentData = ref("Initial Value");
</script>
子组件
<template>
<input :value="modelValue" @input="updateValue" />
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({ modelValue: String });
const emit = defineEmits();
function updateValue(event) {
emit("update:modelValue", event.target.value);
}
</script>
在这个例子中,父组件的数据通过 v-model 传给子组件,子组件只通过 emit 事件请求父组件更新数据,并不会直接修改父组件的数据,从而保持了单项数据流的特性。
结论
v-model
的设计确保了 Vue 的单项数据流不会被破坏。它将父到子的 props
传递和子到父的事件通知机制封装成了更简洁的语法,提升了代码的可读性和维护性。在使用 v-model
时,Vue 依然保持了父子组件之间数据流动的方向和控制,从而不会破坏单项数据流的特性。
42. Vue ref
的实现原理
在 Vue 3 中,ref
是用于实现响应式数据的一种 API。它通过封装数据、依赖收集、追踪变化,实现了自动更新视图的效果。Vue 3 的响应式系统使用了 Proxy 代理,ref
依赖于 Proxy 以及 Vue 的响应式系统来实现。
1. ref
的定义与结构
ref
是一个可以封装任意数据的响应式对象,它会将原始数据包装在一个带.value
属性的对象中,方便读取和修改。- 通过
ref
创建的对象会自动追踪对.value
的访问和修改,从而在数据发生变化时触发视图更新。
2. ref
的创建过程
ref
函数接受一个初始值作为参数,并返回一个包含.value
属性的对象。内部会为这个对象创建依赖追踪对象(dep),用于记录依赖该ref
的所有副作用函数。- 在读取
.value
时触发依赖收集(track),在写入.value
时触发依赖更新(trigger)。
3. 响应式追踪与依赖收集
- Vue 会在每个
ref
实例上创建依赖追踪对象,用于记录哪些地方依赖了该ref
的值。 - 当读取
.value
时,会触发“依赖收集”,将当前活跃的副作用函数添加到依赖追踪对象中。 - 当
.value
被修改时,会触发“依赖触发”,通知依赖的副作用函数重新执行,从而更新视图。
4. 使用 Proxy 实现响应性
- Vue 使用
Proxy
来监听.value
的读取和修改,通过get
捕获器实现依赖收集,通过set
捕获器通知依赖更新。 - 这种基于
Proxy
的机制确保了ref
能够实时追踪和触发数据变化。
5. 对象类型与原始类型的差异
- 当
ref
包装的是对象类型时,会通过reactive
自动将对象内部的属性也转换为响应式。 - 当
ref
包装的是原始数据类型(如数字、字符串)时,只会对.value
属性进行响应式追踪。
6. ref
的自动解包(Unwrapping)
- 在 Vue 模板中,
ref
会自动解包(unwrapping),因此在模板中直接使用变量名即可,不需要.value
。 - 在 JavaScript 中,仍需要通过
.value
访问ref
的值。
7. toRefs
与 reactive
的配合
toRefs
可以将reactive
对象的属性转换为ref
,确保解构时响应性不会丢失。ref
和reactive
的配合使用提升了 Vue 3 的响应式系统的灵活性。
8. ref
实现的核心源码(简化示例)
function ref(value) {
const dep = new Set();
return {
get value() {
track(dep); // 依赖收集
return value;
},
set value(newValue) {
if (newValue !== value) {
value = newValue;
trigger(dep); // 触发依赖更新
}
},
};
}
ref
的实现依赖于 Vue 3
的响应式系统,使用 Proxy
和依赖追踪机制来实现响应性。它在读取 .value
时追踪依赖,在写入 .value
时触发更新,确保了数据变动时视图可以自动响应变化。
43. Vue 3 reactive
的实现原理
Vue 3 中的 reactive
是一个用于将对象转换为响应式的核心 API。reactive
的实现基于 JavaScript 的 Proxy 对象,通过代理原始对象,追踪属性的读取和写入操作,以实现响应式数据的依赖收集和触发更新。
1. reactive
的基本工作原理
reactive
接受一个对象(可以是嵌套对象)作为参数,并返回该对象的代理对象。- 代理对象在数据访问和更新时,会自动触发 Vue 的依赖收集和通知更新机制,从而在数据变化时更新相关的视图。
2. Proxy 实现数据劫持
- Vue 使用
Proxy
来劫持对对象属性的访问和修改。 reactive
会创建一个 Proxy 代理对象,并在get
捕获器中进行依赖收集(追踪哪些地方使用了该数据),在set
捕获器中进行依赖触发(通知依赖该数据的地方进行更新)。
3. 响应式追踪与依赖收集
- 在 Vue 的响应式系统中,每个响应式对象的每个属性都维护一个 依赖集合(Dep),用于记录所有依赖该属性的 副作用函数(effect)。
- 当一个属性被读取时,Vue 会将当前活跃的副作用函数添加到这个属性的依赖集合中。
- 当属性被修改时,Vue 会触发所有相关依赖集合中的副作用函数,使它们重新执行以更新视图。
4. 深度响应性
reactive
会递归地将嵌套对象也转换为响应式对象。- Vue 在 Proxy 的
get
捕获器中进行检查,如果属性的值是对象且尚未代理过,会递归调用reactive
使其也成为响应式对象。
5. Track 和 Trigger 的实现
- Track(追踪):当
reactive
对象的属性被访问时,Vue 会通过track
函数将当前副作用函数注册到这个属性的依赖集合中。 - Trigger(触发):当
reactive
对象的属性被更新时,Vue 会通过trigger
函数触发该属性的依赖集合中的副作用函数,确保视图能够随数据的变动而更新。
6. reactive
的核心源码(简化示例)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 依赖收集
const result = Reflect.get(target, key, receiver);
if (typeof result === "object" && result !== null) {
return reactive(result); // 递归使嵌套对象响应式
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发依赖更新
}
return result;
},
});
}
7. reactive 与 ref 的区别
- reactive 用于将对象转为深度响应式对象,其属性可以是任何类型。
- ref 用于处理原始类型或单一值,且会将数据封装在 .value 属性中。
44. Vue 3 computed
的实现原理
在 Vue 3 中,computed
是用于创建基于其他响应式数据的 派生数据 的 API。computed
的实现依赖于 响应式依赖追踪系统,结合 缓存机制 和 延迟求值,确保在依赖数据不变的情况下避免不必要的计算。
1. computed
的基本概念
computed
接受一个 getter 函数(或带有get
和set
的对象)用于定义计算属性。computed
属性基于依赖数据变化自动更新,并在依赖数据不变时缓存上次的计算结果。
2. computed
的核心机制
computed
是通过 副作用函数(effect) 和 缓存机制 实现的。Vue 为每个 computed
属性创建一个副作用函数,并利用 ReactiveEffect
类管理依赖收集和更新逻辑。
- 依赖收集:
computed
会在首次访问时自动追踪依赖的数据属性,将这些依赖添加到计算属性的追踪列表中。 - 依赖更新:当依赖数据发生变化时,
computed
会标记为“脏”状态,在下次访问时重新计算。
3. computed
的延迟求值与缓存
- 延迟求值:
computed
采用 lazy evaluation(惰性求值),即仅在computed
属性首次被访问时,才会执行其内部的 getter 函数进行计算。 - 缓存机制:在依赖数据未发生变化时,
computed
会直接返回缓存的上一次计算结果。只有在依赖变化后才标记为“脏”,并在下一次访问时重新计算。
4. Track 与 Trigger
- Track(追踪依赖):
computed
属性的 getter 中访问依赖数据时,Vue 的响应式系统会自动将依赖添加到computed
的依赖集合中。 - Trigger(触发更新):当依赖数据发生变化时,Vue 会触发更新,将
computed
标记为“脏”,下次访问时触发重新计算。
5. computed
的核心源码实现(简化示例)
function computed(getter) {
let value;
let dirty = true; // 标记是否需要重新计算
const effect = new ReactiveEffect(() => {
value = getter(); // 重新计算
dirty = false; // 重置为非“脏”状态
});
return {
get value() {
if (dirty) {
effect.run(); // 如果是“脏”状态,则重新计算
}
track(effect); // 收集依赖
return value;
},
};
}
6. computed
与 watch
的区别
computed
:用于计算和缓存依赖其他响应式数据的值,适用于派生数据。依赖不变时,返回缓存结果。watch
:用于侦听数据的变化并执行副作用逻辑,适合处理非派生性的业务逻辑。
总结
Vue 3 中 computed
的实现依赖于响应式系统,通过 依赖追踪 和 延迟求值 优化性能,结合缓存机制确保在依赖不变的情况下避免重复计算。这使得 computed
成为了 Vue 3 中处理派生数据的高效工具。