Skip to content

前端面试 Vue 篇

1. 对 MVVM 的理解

MVVM(Model-View-ViewModel)是一种软件架构设计模式,最早由微软在开发 WPF(Windows Presentation Foundation)和 Silverlight 框架时提出,广泛应用于前端开发,尤其是 Vue、Angular 等前端框架。

MVVM 的三层结构

  1. Model(模型)
    主要负责数据的获取和处理,它通常是指业务数据的模型,包含应用的数据结构和业务逻辑。Model 中的任何变化都会通过数据绑定自动更新到 View 层。

  2. View(视图)
    View 负责用户界面的呈现,它在 MVVM 中是完全“哑”的,也就是不包含任何业务逻辑,只负责展示数据和响应用户的操作。

  3. 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 的工作流程

  1. 用户首次访问页面时,服务器返回一个包含 HTML、CSS、JavaScript 的初始页面。
  2. 之后的用户操作不再请求新页面,而是通过 JavaScript 来动态更新页面内容。
  3. 前端的路由管理会监听 URL 的变化(通常通过 Hash 或 History API),并根据路径加载相应的内容。
  4. 数据通过 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)功能说明
beforeCreatebeforeCreateonBeforeMount实例初始化之前调用,数据观测和事件初始化尚未完成,通常不常用。
createdcreatedonMounted实例创建完成,此时可以访问数据和方法,DOM 尚未渲染,常用于数据初始化。
beforeMountbeforeMountonBeforeUpdate模板编译结束并将要挂载到页面上,DOM 尚未插入,可以在此阶段做挂载前的准备工作。
mountedmountedonUpdated实例挂载完成,DOM 已插入页面。常用于 DOM 操作或与其他库的交互。
beforeUpdatebeforeUpdateonBeforeUnmount数据更新但 DOM 尚未渲染更新,可用于在数据更新前执行操作,比如清除无效的状态。
updatedupdatedonUnmounted数据更新且 DOM 重新渲染完成,适合执行基于最新 DOM 的操作。
activatedactivatedonActivated<keep-alive> 缓存组件激活时调用,常用于处理缓存组件的状态恢复。
deactivateddeactivatedonDeactivated<keep-alive> 缓存组件停用时调用,适合用于保存组件状态。
beforeDestroy替换为 beforeUnmountonBeforeUnmount实例销毁前调用,可用于清除计时器、事件监听等,避免内存泄漏。
destroyed替换为 unmountedonUnmounted实例销毁后调用,此时所有事件监听和响应式数据解绑,适合用于资源释放。
errorCapturedonErrorCaptured捕获子组件中的错误,常用于错误处理和日志记录。
renderTrackedonRenderTracked在响应式依赖被追踪时调用,主要用于调试,查看哪些状态引发了渲染。
renderTriggeredonRenderTriggered在渲染被触发时调用,主要用于调试,帮助分析哪些状态变动引发了渲染。

主要功能变化

  1. 销毁阶段钩子名称变化

    • Vue 2 中的 beforeDestroydestroyed 被替换为 Vue 3 中的 beforeUnmountunmounted,更贴合实例的挂载和卸载过程。
  2. Composition API 的灵活钩子
    Vue 3 提供了 onXxx 系列钩子函数,在 Composition API 中可以直接调用,如 onMountedonUpdated,方便逻辑复用。尤其适合在逻辑复杂、组件拆分较细的场景中使用。

  3. 新增调试相关钩子

    • renderTracked:追踪响应式依赖,帮助开发者了解渲染过程中依赖的变化。
    • renderTriggered:渲染被触发时调用,可用于分析引发渲染的原因和依赖关系,辅助性能优化。

结论

Vue 3 在保留 Vue 2 主要生命周期功能的同时,增强了生命周期的灵活性和调试能力,特别是在 Composition API 中提供的钩子,可以让生命周期管理更加便捷和可维护。

4.子组件以及父组件更新触发生命周期的过程

在 Vue 中,父组件和子组件的生命周期钩子会按一定的顺序触发。理解父子组件的生命周期触发顺序有助于避免不必要的重渲染和数据问题。

组件挂载过程中的生命周期触发顺序

当父组件首次挂载并渲染子组件时,生命周期钩子的触发顺序如下:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

总结:父组件的初始化钩子(beforeCreatebeforeMount)会先于子组件的钩子执行;子组件的 mounted 先于父组件的 mounted 执行。


组件更新过程中的生命周期触发顺序

当父组件或子组件中的响应式数据发生变化时,更新过程中的生命周期触发顺序会略有不同:

1. 父组件数据变化导致父组件和子组件更新

当父组件的数据变化,并且影响到子组件的内容时,钩子触发顺序如下:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

总结:父组件的 beforeUpdate 钩子先触发,然后进入子组件的 beforeUpdateupdated,最后是父组件的 updated 钩子。这保证了子组件的更新在父组件完成之前完成。

2. 子组件数据变化导致子组件更新

当子组件自身的数据变化时,触发的生命周期如下:

  1. 子组件 beforeUpdate
  2. 子组件 updated

总结:如果只是子组件的数据变化,不会影响父组件,因此只触发子组件的 beforeUpdateupdated 钩子。


组件销毁过程中的生命周期触发顺序

当父组件被卸载(或条件渲染下某个子组件被卸载)时,生命周期钩子触发顺序如下:

  1. 父组件 beforeUnmount
  2. 子组件 beforeUnmount
  3. 子组件 unmounted
  4. 父组件 unmounted

总结:父组件在销毁阶段先触发 beforeUnmount,然后是子组件的 beforeUnmountunmounted 钩子,最后再触发父组件的 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() 来实现响应式。主要原理如下:

  1. 数据劫持
    Vue 2 通过 Object.defineProperty 劫持每个数据属性的 getset 方法,在访问或修改数据时触发相关逻辑。

  2. 依赖收集
    每个响应式属性都有一个 Dep 实例来存储该属性的依赖(即观察者)。当组件渲染时,会触发属性的 get 方法,将依赖(Watcher)收集到 Dep 中。

  3. 数据更新
    当属性的 set 方法被调用时,Dep 通知所有依赖该属性的 Watcher,从而触发视图更新。

Vue 2 响应式的局限

  • 数组和对象的深层次监听问题
    Object.defineProperty 不能监听对象和数组的新增或删除,需要 Vue 提供额外的方法(如 Vue.setVue.delete)来解决。
  • 性能问题
    在大规模数据处理时,需要递归遍历整个对象树来进行深度监听,容易导致性能问题。

Vue 3 的响应式原理

Vue 3 使用 Proxy 实现响应式系统,这种实现方式更加灵活,克服了 Object.defineProperty 的局限性。主要原理如下:

  1. 响应式转换
    Vue 3 使用 Proxy 代理整个对象,将 getset 操作委托给代理,从而可以直接监听新增和删除的属性。

  2. 依赖收集和触发
    get 拦截器中进行依赖收集,当某个属性被访问时,会将依赖存储在一个全局的 WeakMap 中。

    • WeakMap 的键是响应式对象,值是存储依赖的 Map
    • Map 的键是对象的属性名,值是该属性的依赖集合(即 Set,用于存储 Effect)。
  3. 性能优化
    Proxy 不需要递归整个对象即可监听嵌套属性,只有当嵌套属性被访问时才会创建代理,从而减少了性能开销。

Vue 3 响应式的优点

  • 支持数组和对象的新增、删除属性
    Proxy 能够直接监听对象的结构变化,无需通过额外方法(如 Vue.set)来处理新增或删除的属性。
  • 按需监听
    只有在属性被访问时才进行代理处理,大大减少了不必要的性能开销,提升了性能。

Vue 2 和 Vue 3 响应式的主要对比

特性Vue 2 (Object.defineProperty)Vue 3 (Proxy)
实现方式Object.definePropertyProxy
深层监听需要递归遍历整个对象自动支持,按需代理
数组操作需要使用 Vue.setVue.delete 处理自动支持新增和删除
性能数据量大或层级深时性能较差更高效,减少不必要的代理和递归
兼容性支持 IE 11不支持 IE 11

总结

Vue 2 使用 Object.defineProperty 实现响应式,在某些情况下需手动处理数组和对象的属性增删,且大规模数据下性能有限。而 Vue 3 通过 Proxy 提供了更完善的响应式系统,提升了性能并增强了开发体验,尤其在复杂数据结构的监听和高效性上表现更佳。

6. Proxy 只会代理对象的第⼀层,那么 Vue3 ⼜是怎样处理这个问题的呢?

虽然 Proxy 只能直接代理对象的第一层属性,但 Vue 3 通过按需递归代理(惰性代理)的机制,实现了深层次的响应式支持。以下是 Vue 3 如何处理这一问题的详细说明:

Vue 3 的按需递归代理机制

  1. 惰性代理
    Vue 3 通过 Proxy 代理对象的第一层属性。当访问到嵌套属性时(触发 get 操作),Vue 会判断该属性是否已被代理。如果该嵌套属性是普通对象且未被代理,那么 Vue 会在此时为它创建一个新的 Proxy,使其成为响应式对象。这种方式即按需递归代理,避免了初始化时的深度递归遍历。

  2. 依赖收集与响应式追踪
    当代理对象的属性被访问时,Vue 的响应式系统会收集依赖,确保数据变动时能够触发相关更新。如果嵌套属性被代理后发生变化,Vue 会通过 Proxyset 拦截器触发响应式更新,确保视图随嵌套数据变化而自动更新。

优点

  • 性能优化
    按需递归代理只在嵌套属性被访问时才创建代理,避免了初始化时对整个对象的深度遍历和代理处理,节省了性能开销。
  • 深层嵌套支持
    通过这种惰性代理,Vue 3 能够自动支持任意深层级的嵌套数据。只要被访问的嵌套属性会自动变为响应式,解决了 Proxy 仅代理第一层的问题。

总结

Vue 3 通过按需递归代理的机制,使得响应式系统能够动态生成嵌套属性的代理对象,保证了深层数据的响应式能力,同时避免了性能开销。这种机制是 Proxy 与 Vue 3 响应式系统结合的核心优势所在。

7. 监测数组的时候可能触发多次 get/set,那么如何防⽌触发多次呢?

我们可以判断 key 是否为当前被代理对象 target ⾃身属性,也可以判断旧值与新值是否相等,只有满⾜以上两个条件之⼀时,才有可能执⾏ trigger。

8. Vue 2 与 Vue 3 区别对比表

特性Vue 2Vue 3
响应式系统使用 Object.defineProperty,无法监听新增或删除的属性使用 Proxy,支持监听新增、删除属性及嵌套属性的变化
性能性能较差,尤其在复杂数据和大量更新时提升了性能,使用 Proxy 和优化的虚拟 DOM 渲染
API 风格使用 Options API,通过 datamethods 等定义组件逻辑引入 Composition API,通过 setuprefreactive 等更灵活地组织组件逻辑
TypeScript 支持支持较差,需要额外的工具,如 vue-class-componentvue-property-decorator原生支持 TypeScript,类型推导更为完善
生命周期钩子使用传统的生命周期钩子:createdmounted新增 setup 钩子,生命周期钩子通过 onMountedonUpdated 等在 setup 内使用
Fragment 支持只能有一个根元素支持 Fragment,一个组件可以有多个根元素
Teleport 和 Suspense不支持支持 TeleportSuspense,处理异步组件和跨位置渲染,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 中怎么异步加载组件

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 的写法
vue
<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 中,watchcomputed 都是响应式机制的一部分,主要用于监听和计算数据的变化。然而,它们的应用场景和工作方式有所不同。

1. computed 的特性

定义:

computed 用于定义基于响应式数据的计算属性。它会根据其依赖的数据变化而重新计算,并且计算结果会被缓存,直到依赖的数据变化为止。

特点:

  • 缓存:计算属性的值会缓存,只有当它所依赖的响应式数据变化时,才会重新计算。
  • 同步computed 计算属性的求值是同步的。
  • 性能优化:当多个地方依赖于同一个计算属性时,computed 会避免重复计算,只有依赖的数据变化时才会重新计算。

使用场景:

  • 适合用于计算和返回基于响应式数据的衍生数据。

2. watch 的特性

定义:

watch 用于观察和响应数据的变化。当某个数据发生变化时,watch 会执行相应的回调函数。watch 适用于执行异步操作或进行副作用(如数据更新、API 调用等)时。

特点:

  • 不缓存watch 监听的是某个响应式数据的变化,每次变化都会触发回调函数,且回调是异步执行的。
  • 副作用watch 适用于处理副作用操作,如发起网络请求、操作外部 API 等。
  • 异步watch 的回调是异步的,可以用于处理需要执行副作用的操作。

使用场景:

  • 适合用于执行副作用、异步任务或者某些数据变化后的操作。

3. computedwatch 的对比

特性computedwatch
主要用途用于计算衍生状态,基于已有数据计算返回值用于监听数据变化,处理副作用或异步操作
缓存会缓存,只有依赖的响应式数据变化时才会重新计算不缓存,每次数据变化都会触发回调
计算类型同步计算,结果会立即返回异步执行,适合需要执行副作用的任务
使用场景适用于计算属性(如合成数据、格式化等)适用于观察数据变化并执行副作用(如异步请求)
触发时机当计算属性的依赖数据变化时自动重新计算当监听的响应式数据发生变化时执行回调

总结

  • computed:适用于基于现有数据计算衍生数据,具有缓存特性,不会重复计算,适合需要性能优化的场景。
  • watch:适用于监听数据变化并执行副作用(如异步操作、API 请求等),可以用于处理复杂的逻辑或与外部环境交互。

根据实际需求选择合适的方式,computed 用于计算,watch 用于观察和执行操作。

15. Vue 3 中 reftoReftoRefsreactive 的总结

在 Vue 3 的 Composition API 中,reftoReftoRefsreactive 都用于创建和管理响应式数据。它们的主要区别在于使用场景、返回值类型以及是否支持深度响应式。

总结对比

特性refreactivetoReftoRefs
适用类型基本数据类型、对象、数组等对象、数组等复杂数据响应式对象中的单个属性将整个 reactive 对象的属性转换为 ref
返回值类型ref 对象响应式对象返回 ref返回对象,属性为 ref
深度响应式不会深度代理(基本类型数据)深度代理整个对象仅代理指定属性代理对象中所有属性
使用场景创建简单响应式变量对象或数组的深度响应式管理将对象中的属性转为单独的 ref批量将 reactive 对象的属性转换为 ref
  • ref:用于创建基本数据类型的响应式变量,或将对象、数组等包装为响应式引用。
  • reactive:用于创建整个对象或数组的深度响应式,适合管理复杂的数据结构。
  • toRef:将 reactive 对象的某个属性转换为单独的 ref,保持原对象的响应式。
  • toRefs:将 reactive 对象的所有属性转换为 ref,适合批量操作和解构对象。

根据数据类型和需求的不同,选择合适的方式来管理响应式数据。

16. Vue 中怎么做全局错误监听?

javascript
// 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 中怎么访问实例?

javascript
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const appContext = instance.appContext;

18. 怎么监听子组件内的错误?

javascript
// 子组件
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 会将其从缓存中恢复,而不是重新创建。

使用 includeexclude 控制缓存

  • keep-alive 可以通过 includeexclude 属性来指定哪些组件需要缓存,哪些组件不需要缓存。
    • include:一个字符串、正则表达式或数组,指定哪些组件应被缓存。
    • exclude:一个字符串、正则表达式或数组,指定哪些组件不应被缓存。

生命周期钩子

  • keep-alive 缓存的组件中,会触发一些特定的生命周期钩子:
    • activated:当组件从缓存中恢复并显示时,触发此生命周期钩子。
    • deactivated:当组件被缓存(即切换出视图)时,触发此生命周期钩子。

如何工作

  1. 当一个组件包裹在 keep-alive 中并第一次渲染时,组件会正常初始化并挂载。
  2. 当切换离开这个组件时,keep-alive 会将该组件缓存起来,而不是销毁它。
  3. 当再次切换回这个组件时,keep-alive 会从缓存中恢复该组件的状态,而不是重新创建组件实例。

2. 实现方式

Vue 通过对组件的 createdmounteddestroyed 等生命周期钩子的处理,配合 vnode(虚拟节点)缓存机制来实现组件缓存。

  • 缓存组件实例:当组件被包裹在 keep-alive 中时,Vue 会在内部维护一个缓存池。每个组件实例都会根据其 key 值被存储。
  • 复用实例:当组件需要被复用时,Vue 会直接从缓存池中取出实例,而不是重新创建一个新的实例。
  • 组件销毁与恢复:在 keep-alive 内部,只有组件被切换离开并且不再活跃时,它才会被缓存。再次显示该组件时,会触发其 activated 钩子,恢复之前的状态。

3. 常见使用场景

  • Tab 页面切换:当用户在多个 Tab 之间切换时,可以通过 keep-alive 缓存已经访问过的 Tab 页面,避免每次切换时重新加载数据。
  • 表单填写:对于较长的表单填写,可以保持表单的输入状态,避免用户每次返回时丢失已填内容。

优先级规则

keep-alive 中,exclude 的优先级高于 include

4. 总结

  • 缓存机制keep-alive 通过缓存组件实例来避免重复的创建和销毁,提升性能。
  • 生命周期钩子activateddeactivated 允许开发者在组件恢复和离开时执行特定的操作。
  • 控制缓存includeexclude 提供了灵活的缓存控制。

keep-alive 是 Vue 提供的优化性能的利器,特别适用于需要频繁切换的组件,通过减少不必要的组件销毁和重建,大大提升用户体验。

20. vue-router 怎么动态添加、删除路由?

javascript
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 数据丢失怎么处理?

  1. 使用第三方插件 进行 持久化数据 如 vuex-persistedstate

  2. 写插件 注册时取、离开时存

  • vuex 注册时,从缓存取数据 进行 merge
  • 监听页面刷新状态 页面离开之前 存数据
  • 使用 localstorage、sessionstorage 进行存储

22. SPA 怎么优化首页白屏时间?

最终解决方案:SSR 服务端渲染

SPA: Single Page Application(单页应用),Vue 和 React 都是。 Vue 项目中,我们打完包生成 dist 时,如果没有指定多入口,就只会生成一个 html 文件,和一些 js、css、图片等资源文件。html 中并没有渲染元素,就一个 id 为 app 的 div。页面上的元素都是通过请求 js 动态渲染上去。所有本身就挺慢的。

  • 路由懒加载,首屏只请求首页相关 jscss
  • 骨架屏优化交互体验
  • 组件异步加载,首页先渲染首屏的资源,首屏下面的组件异步加载
  • 尽量减少 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 会缓存当前的渲染结果,而不是重新渲染视图。
vue
<template>
  <div v-memo="[dependency1, dependency2]">
    <!-- 这里的内容会被缓存,直到依赖项变化 -->
  </div>
</template>

总结

  • 性能优化v-memo 提供了减少不必要渲染的能力,尤其在复杂视图或频繁变化的数据场景中,可以显著提高性能。
  • 依赖控制:通过指定依赖项,开发者可以精确控制何时重新渲染视图,避免过度渲染带来的性能损失。

24. 说一下 Vue3 声明一个响应式数据的方式?

  • ref: 通过.value 访问及修改
  • reactive: 直接访问、只能声明引用数据类型
  • computed: 也是通过.value,声明需要 传 get、set
  • toRef: 类似 ref 的用法,可以把响应式数据的属性变成 ref
  • toRefs: 可以把响应式数据所有属性 转成一个个 ref
  • shallRef: 浅层的 ref,第二层就不会触发响应式
  • shallReactive: 浅层的 reactive,第二层就不会触发响应式
  • customRef: 自定义 ref

25.说一下 watch 和 watchEffect?

特性watchwatchEffect
目标监听一个或多个指定的数据源变化自动追踪所有响应式数据的变化
需要传入数据源需要明确指定监听的响应式数据或计算属性无需传入数据源,会自动追踪作用域中的所有依赖
依赖收集手动指定依赖自动收集依赖,回调会自动执行
回调函数参数可访问 oldValuenewValue不支持 oldValuenewValue,只会执行副作用
执行时机默认异步执行,回调会被推入微任务队列同步执行,立即触发副作用

总结

  • 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-ifv-for)和数据绑定(如 )转换成 JavaScript 渲染函数,最终生成的渲染函数用于创建和更新虚拟 DOM。

Vue 模板编译过程主要分为三个阶段:

  1. 解析阶段:将模板字符串解析成抽象语法树(AST)。
  2. 优化阶段:对 AST 进行优化,标记静态节点。
  3. 代码生成阶段:根据优化后的 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.xVue 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 APIHash 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 路由通过 pushStatehash 来管理历史记录,从而实现路径的变化和视图的切换。Vue Router 会在浏览器的历史记录中加入新的记录,并且允许在路由跳转时不会重新加载页面。

1.5 导航守卫

Vue 路由还支持导航守卫,用于在路由变化时进行钩子操作(如权限校验、数据加载等)。常见的导航守卫有:

  • 全局守卫beforeEachafterEach
  • 路由独享守卫beforeEnter
  • 组件内守卫beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

1.6 懒加载与动态路由

Vue Router 支持懒加载和动态路由组件。通过 route-level code-splitting,路由组件可以按需加载,减少初始加载时的资源体积。

2. Vue 路由模式

Vue Router 支持多种路由模式,主要有以下几种:

2.1 Hash 模式

  • 原理:Hash 模式利用 URL 中的 # 符号来标识路由路径,改变 # 后的路径不会导致页面重新加载,而是触发路由匹配。 用hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)
  • 优点
    • 不需要服务器配置,能兼容所有浏览器。
    • 路由的变化不会导致页面刷新。
  • 缺点
    • URL 中会带有 #,不够美观。
    • 不支持服务端渲染时的 SEO 优化。

工作原理

  • 访问的 URL 会是 http://example.com/#/home,浏览器会监听 # 后的变化来触发路由更新。

2.2 History 模式

pushStaterepalceState 两个 API 来操作实现 URL 的变化, 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染), history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

popstate 事件的触发时机
  • 触发条件popstate 事件会在浏览器的历史记录发生变化时触发,通常发生在以下情况:

    • 用户点击浏览器的“后退”或“前进”按钮。
    • 使用 history.back()history.forward()history.go() 等方法进行导航。
  • 注意pushState()replaceState() 只是改变历史记录,而不会直接触发 popstate 事件。只有当浏览器的历史记录栈发生变化时(例如用户进行前进/后退操作),popstate 事件才会被触发。

  • 原理:History 模式使用 HTML5 的 history.pushStatehistory.replaceState 方法来改变浏览器的历史记录,并且不再使用 # 符号。这使得 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 路由的工作原理通过 HashHistory 等模式管理 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. 基本概念与设计理念

特性VuexPinia
设计理念Vuex 是一个完整的状态管理框架,适合中到大型应用Pinia 是轻量级状态管理库,专为 Vue 3 设计
数据存储使用 storemodules 来管理状态使用 store,支持模块化,但更简单直观
依赖依赖 Vue.js 和 Vue Router仅依赖 Vue 3,轻量级,不再需要 Vue Router
语法风格基于 Vuex 的传统 API,使用 mutations、actions 等更简洁现代,使用 defineStore,API 更直观

2. API 与使用方式

特性VuexPinia
创建 storenew Vuex.Store({...})defineStore({...})
存储方式使用 stategettersmutationsactions使用 stategettersactions(支持模块化)
类型支持需要手动配置类型支持,Vuex 3.x 不完全支持 TS内建支持 TypeScript,自动推导类型
模块化支持 store 模块化更简洁,模块化非常轻量,直接支持复用

3. 开发体验与功能特性

特性VuexPinia
类型推导支持需要手动配置,类型支持较弱强大的类型推导,使用 TypeScript 更加便捷
热重载热重载支持较差(需要插件支持)原生支持热重载,开发体验更好
插件支持支持各种插件,如 vuex-persistedstate轻量,内置支持 DevTools,可扩展插件支持
DevTools 支持支持 Vuex DevTools,但较为复杂原生集成 Vue DevTools,操作更为简便

4. 性能与优化

特性VuexPinia
性能虽然性能优秀,但存在较高的内部复杂性更轻量,优化了响应性和性能
响应性使用 Vue.setmutations 处理响应性基于 Vue 3 的 Proxy,响应性更高效
模块化管理Vuex 支持复杂的模块化,适合大型项目更简单直观的模块化管理,避免过多的复杂配置

5. 学习曲线

特性VuexPinia
学习曲线比较陡峭,需要了解 Vuex 的基础概念(state、mutations、actions)学习曲线较平缓,API 设计简单,特别适合新手
文档完善,但学习时需要花费时间理解 Vuex 的概念和实现更简洁的文档,易于上手,配合 Vue 3 使用更直观

6. 总结

特性VuexPinia
推荐使用场景适合中到大型应用,需要复杂的状态管理和插件支持更适合 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

javascript
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 中的 datamethodscomputed 等选项式 API.

1. setup 函数的基本用法

setup 函数在组件创建时被调用,接收两个参数:propscontext

javascript
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 默认绑定 modelValueupdate: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-modelemits 配合

  • 在子组件中,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 动态绑定的值。

vue
<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、事件等动态传递给被渲染的组件。

vue
<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> 标签渲染父组件传递的内容。

vue
<template>
  <div>
    <slot></slot>
  </div>
</template>

父组件使用默认插槽传递内容:

vue
<template>
  <ChildComponent>
    <p>This is default slot content</p>
  </ChildComponent>
</template>

2.2 命名插槽

命名插槽使得父组件可以选择性地将内容传递给子组件的不同插槽。通过 slot 属性为插槽指定名称。

子组件定义命名插槽:

vue
<template>
  <div>
    <slot name="header"></slot>
    <slot name="footer"></slot>
  </div>
</template>

父组件传递命名插槽的内容:

vue
<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 将数据传递给父组件。

vue
<template>
  <div>
    <slot :message="message"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello from Scoped Slot",
    };
  },
};
</script>

父组件通过插槽的作用域接收数据,并使用数据:

vue
<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 的实现机制基于 propsevents 的双向绑定,具体通过以下两个步骤来实现父子组件的数据同步:

  1. 父组件传递数据到子组件:父组件通过 v-model 绑定,将数据传递给子组件。子组件通过 modelValue 属性接收该数据。

  2. 子组件通过事件通知父组件更新数据:当子组件中的数据发生变化时,通过触发 update:modelValue 事件,将更新的数据传递回父组件。父组件接收到这个事件后,更新自身的数据,再次通过 props 将更新后的数据传递给子组件。

v-model 对单项数据流的影响

v-model 依然遵循 Vue 的单项数据流设计:

  • 数据流动方向:数据始终是从父组件传递到子组件。
  • 更新流动方向:数据更新是通过事件的方式从子组件传递到父组件,再由父组件更新数据并传递回子组件。
父组件
vue
<!-- 父组件 -->
<template>
  <ChildComponent v-model="parentData" />
</template>

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

const parentData = ref("Initial Value");
</script>
子组件
vue
<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. toRefsreactive 的配合

  • toRefs 可以将 reactive 对象的属性转换为 ref,确保解构时响应性不会丢失。
  • refreactive 的配合使用提升了 Vue 3 的响应式系统的灵活性。

8. ref 实现的核心源码(简化示例)

typescript
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 的核心源码(简化示例)

typescript
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 函数(或带有 getset 的对象)用于定义计算属性。
  • 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 的核心源码实现(简化示例)

typescript
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. computedwatch 的区别

  • computed:用于计算和缓存依赖其他响应式数据的值,适用于派生数据。依赖不变时,返回缓存结果。
  • watch:用于侦听数据的变化并执行副作用逻辑,适合处理非派生性的业务逻辑。

总结

Vue 3 中 computed 的实现依赖于响应式系统,通过 依赖追踪延迟求值 优化性能,结合缓存机制确保在依赖不变的情况下避免重复计算。这使得 computed 成为了 Vue 3 中处理派生数据的高效工具。

Updated at:

Released under the MIT License.