Vue 面试前复习

Vue 模块复习

嘛!技术栈主要是vue,结合最近面试的问题,做一些面试总结,顺便复习一下!

Vue 生命周期

嘛,从我个人的开发经验来说,Vue2 和 Vue3 其实生命周期钩子,在选项式api模式下大差不差对比表格:

Vue2选项式 Vue3选项式 Vue3组合式 作用
beforeCreate beforeCreate setup() 在实例初始化之后,数据观测和事件配置之前被调用
created created onBeforeMount 在实例创建完成后被立即调用,可在此执行数据初始化
beforeMount beforeMount onBeforeMount 在挂载开始之前被调用,相关render函数首次被调用
mounted mounted onMounted 组件挂载到DOM后调用,可获取DOM节点
beforeUpdate beforeUpdate onBeforeUpdate 数据更新时,虚拟DOM重新渲染和打补丁之前调用
updated updated onUpdated 组件DOM更新后调用,可执行依赖于DOM的操作
activated activated onActivated keep-alive组件激活时调用
deactivated deactivated onDeactivated keep-alive组件停用时调用
beforeDestroy beforeUnmount onBeforeUnmount 实例销毁之前调用,可执行清理操作
destroyed unmounted onUnmounted 实例销毁后调用,调用后实例指向销毁,所有东西可回收
errorCaptured errorCaptured onErrorCaptured 捕获子孙组件错误时被调用

为什么 beforeCreate 对标的是 setup

  • beforeCreate 在实例被创建之后,datamethods 还未初始化之前调用
  • setup 在组件创建之后, datamethods 初始化之前被调用

所以 setup 对应于 beforeCreate 钩子。

为什么 created 对标的是 onBeforeMount

  • created 在组件实例被创建之后调用,这个时候还没有开始 DOM 的挂载,data 数据对象就已经被初始化好了。
  • onBeforeMount 会在组件挂载到 DOM 之前调用,这个时候数据已经初始化完成,但是还没有开始 DOM 渲染。

所以其功能与 created 类似,都是表示实例初始化完成,但还未开始 DOM 渲染。

组件间的通信方式

这个算是很容易被问到的,但是又不怎么问的!

通信方式 说明 优点 缺点
事件总线 利用空Vue实例作为消息总线 简单,低耦合 难维护,调试难度大
provide/inject 依赖注入,可跨多层级 低耦合,方便访问父级数据 无法响应式,只适用于父子孙组件间
本地存储 localStorage、sessionStorage 通用简单 没有响应式,需要手动同步
状态管理工具 Vuex、Pinia等 集中状态管理,高效调试 学习和构建成本较高
父子组件通信 props down, events up 天然的Vue组件通信方式 只能单向,父子组件间才有效

动态组件

通过使用<component>并动态绑定is属性,可以实现动态切换多个组件的功能。

js 复制代码
// 组件对象
const Foo = { /* ... */ } 
const Bar = { /* ... */ }

// 动态组件
<component :is="currentComponent"/>

data() {
  return {
    currentComponent: 'Foo'
  }
}

异步组件

异步组件通过定义一个返回Promise的工厂函数,实现组件的异步加载。

js 复制代码
const AsyncComponent = () => ({
  // 组件加载中         
  component: import('./MyComponent.vue'),
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间
  delay: 200,
  // 加载组件时的提示
  loading: LoadingComponent,
})

然后在组件中使用:

html 复制代码
<async-component></async-component>

当异步组件加载成功后,将显示该组件,否则展示fallback组件。

异步组件常用于路由按需加载和代码分割。

keep-alive

keep-alive是Vue提供的一个内置组件,可以使被包含的组件保留状态,避免反复重渲染,使用 keep-alive 进行缓存的组件会多两个生命周期钩子函数:activated、deactivated

js 复制代码
<!-- 使用keep-alive包裹动态组件 -->
<keep-alive>
  <component :is="currentComponent"></component> 
</keep-alive>

<!-- 动态切换组件 -->
<button @click="currentComponent = 'A'">Show A</button>
<button @click="currentComponent = 'B'">Show B</button>

实现机制

  1. keep-alive组件会在内部维护一个对象
    • cache:用来缓存已经创建的组件实例
  2. 在组件切换时,优先获取include内的组件,过滤exclude内的组件,然后再检查缓存中是否已经有实例
    • 如果有则取出重用
    • 如果没有缓存,则正常创建新实例,并存储到缓存中。
  3. 在组件销毁时,不会立即执行销毁,而是将其保存在缓存中(也要判断include和exclude)
  4. keep-alive 会拦截组件的钩子函数;在适当时机调用 activated 和 deactivated 钩子
  5. 当缓存数量超过上限时,会释放最近最久未使用的缓存实例

slot

slot 是我们在自定义组件,或者使用组件时候最喜欢用到的一个语法了

具名插槽

base-layout:

html 复制代码
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 name<slot> 出口会带有隐含的名字"default"

使用:

js 复制代码
<base-layout>
  <template v-slot:header>
    <h1>header</h1>
  </template>

  <p>paragraph</p>

  <template v-slot:footer>
    <p>footer</p>
  </template>
</base-layout>

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的

current-user:

js 复制代码
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

使用:

js 复制代码
<current-user>
  <template v-slot:default="{ user }">
    {{ user.firstName }}
  </template>
</current-user>

动态插槽

我们可以动态配置 slotName 来进行插槽配置

js 复制代码
<base-layout>
  <template v-slot:[slotName]>
    ...
  </template>
</base-layout>

插槽是如何渲染的呢?

  1. 编译阶段

    • 子组件模板中<slot>会生成一个Slot AST节点
    • 父组件v-slot会生成Template AST节点
    • 两者都会标注slot名称,建立关联
  2. 渲染阶段

    • Vue组件的_render方法会先执行子组件的render
    • render里遇到slot标记会生成comment节点占位
    • 然后执行scoped slot的render,生成父组件传递的slot内容
    • 最后在_update方法Patch时,找到对应评论节点插入内容
  3. 核心流程

    • parse -> slot AST + slot内容AST关联
    • render子组件 -> 插槽节点
    • render父组件内容 -> slot内容
    • patch时插入关联的内容

异步更新队列

这里引用官方的一句话:

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

为什么需要异步更新DOM呢?

假设一个场景

html

html 复制代码
<div>{{ title }}</div>

js

js 复制代码
test() {
   for(let i = 0; i < 100; i++){
      this.title = `第${i}个标题`
   }
}
...
mounted(){
    test()
}

这里我们在 test中使用修改了 title,假设一下,如果没有异步更新这个dom,那么就要操作100次,为了避免这种无意义的性能消耗,Vue再侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果在同一事件循环中多次更新DOM,会导致不必要的计算和DOM操作。将它们 defer 到下一个事件循环执行,可以有效减少开销。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际 (已去重的) 工作

nextTick

nextTick 相信大家都在项目中或多或少的用过几次吧!

nextTick: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

nextTick 实现原理

可以简述如下:

  1. nextTick接收一个回调函数作为参数
  2. 内部会维护一个异步回调队列数组
  3. 将传入的回调推入这个异步队列
  4. 在微任务(promise.then/MutationObserver)空闲时刻执行队列中的回调
  5. 达成在DOM更新后执行回调的效果

写个例子

js 复制代码
let callbacks = [] // 异步回调队列

function nextTick(cb) {
  callbacks.push(cb) // 推入回调队列
  
  // 微任务执行callbacks
  Promise.resolve().then(flushCallbacks) 
}

function flushCallbacks() {
  callbacks.forEach(cb => cb()) // 执行队列回调
  callbacks = [] // 重置队列
}

Vue3 refreactive 区别,如何选择?

个人认为 refreactive 其实 没必要这个都抛出来给我们用,容易造成一些使用困扰,ref我觉就好,不过存在即合理,还是细说一下区别

区别

ref() reactive()
✅支持基本数据类型+引用数据类型 ❌只支持对象和数组(引用数据类型)
❌在 <script><template> 使用方式不同(script中要.value) ✅在 <script><template> 中无差别使用
✅重新分配一个新对象不会失去响应 ❌重新分配一个新对象会丢失响应性
需要使用 .value 访问属性 能直接访问属性
✅传入函数时,不会失去响应 ❌将对象传入函数时,失去响应
✅解构对象时会丢失响应性,需使用toRefs ❌解构时会丢失响应性,需使用toRefs

Vue 模板编译的原理

响应式是 Vue中很重要的一环,但是模板编译也是很重要的一环,从面试的角度来说,Vue的模板编译主要是这几个步骤:

  • 解析:解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码字符串

    • 使用正则等方式解析模板字符串,生成 AST 抽象语法树
    • 遍历 AST,生成渲染函数
  • 优化:优化抽象语法树

    • 检测子节点中是否是纯静态节点
    • 对数据访问点进行转换,生成 getter/setter,实现响应式
    • 使用缓存存放已经编译好的渲染函数,避免重复编译
  • 生成:将渲染函数打包生成新函数,返回函数的字符串形式

    • 依赖响应式系统触发更新,执行渲染函数重新渲染
    • 通过 diff 算法对比新旧节点,最小化更新实际 DOM

Vue、React、Angular 模板编译方式优缺点

框架 模板语法 编译方式 学习曲线
Vue.js 简单 HTML-like 运行时和构建时编译
易于理解
React JSX (JavaScript XML) 编译为 JavaScript 中等
嵌入 JavaScript 中
Angular 复杂,基于 HTML 预编译 (Ahead of Time) 较高
双向数据绑定

Vue diff 算法的过程

Vue2

关于Vue2 的 diff 算法个人的理解上是:

  • 深度优先+双指针(头尾交叉对比)的 diff
    • 深度优先(同层比较):

      • 逐层比较新旧虚拟 DOM 树的节点,对于每一层,它会按照顺序比较节点,找出差异,并标记需要更新的地方。这样的遍历方式有助于更快地发现差异,因为它会首先比较同一层级的节点,然后再递归到下一层级。
        • 通过虚拟节点的key和tag来进行判断是否相同节点
        • 如果相同则将旧节点关联的真实dom的挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组
        • 如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom
    • 双指针:

      • 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,
      • 这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom
      • 之后的节点对比就是深度优先对比的步骤

这样一直递归的遍历下去,直到整棵树完成对比。

流程图

graph TD A[旧的虚拟 DOM] -->|Diff 过程| B(新的虚拟 DOM) B -->|比较节点| C{节点相同?} C --> | 否 | D[标记为更新] C --> | 是 | E[标记为不变] E -->|继续到下一个节点| B D -->|递归比较子节点| B B -->|结束 Diff 过程| F[结束 Diff 过程] F -->|更新后的虚拟 DOM| G[更新后的虚拟 DOM]

Vu3

  • 双指针+最长递增子序列 diff算法
    • 预处理前前置节点
      • 从头对比找到有相同的节点 patch ,发现不同,立即跳出
    • 预处理后置节点
      • 如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环
    • 处理仅有新增节点的情况
      • 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。
    • 处理仅有删除节点的情况
      • 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )
    • 处理新增、删除、移动混合的情况
      • 把没有比较过的新的vnode节点,建立一个数组,每个子元素都是 0 里面的数字记录老节点的索引 ,数组索引就是新节点的索引
        • map = [0,0,0,0]
      • 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex
      • 没有找到与老节点对应的新节点,卸载当前老节点
      • 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中
        1. 如果节点发生移动 记录已经移动了
        2. patch新老节点 找到新的节点进行patch节点

Vue 常见的性能优化方式(结合项目场景)

在实际项目中,Vue 的性能优化需要根据具体的场景和需求来选择合适的策略。以下是一些常见的 Vue 性能优化方式,结合项目场景进行总结:

  1. 使用生产环境构建:
    • 在生产环境中使用 Vue 的生产版本,以减少体积和提高性能。
  2. 异步组件和路由懒加载:
    • 对于大型项目,使用异步组件和路由懒加载,以分割代码并实现按需加载,减小初始加载体积。
  3. 合理使用 v-if 和 v-show:
    • 对于频繁切换的元素,使用 v-show,对于不经常切换的元素,使用 v-if,以减少 DOM 元素的挂载和卸载。
  4. 合理使用 v-for:
    • 遍历大数据集时,避免在模板中访问复杂度较高的属性,最好在数据源中进行预处理。 如果数据不变,可以考虑使用 Object.freeze 冻结对象,以防止 Vue 的响应式系统监听它。
  5. 合理使用计算属性和 Watch:
    • 将复杂的计算逻辑放入计算属性,避免在模板中进行复杂的计算。 使用 deep 选项和 immediate 选项来优化 Watcher。
  6. 合理使用事件委托:
    • 在父组件上使用事件委托,将事件处理推移到父组件上,以减少子组件的监听器数量。
  7. 合理使用 keep-alive:
    • 对于频繁切换的组件,可以考虑使用 <keep-alive> 缓存组件实例,以减少组件的销毁和重新创建。
  8. 合理使用缓存
    • 利用缓存机制,例如在数据请求结果中使用缓存,以避免不必要的重复请求。
  9. 合理使用过渡效果和动画:
    • 控制过渡效果和动画的触发时机,避免在大量元素上使用过渡效果,以提高性能。
  10. 优化网络请求
    • 使用合适的数据加载方式,例如分页加载或滚动加载,以降低页面初始化时的请求量。
  11. 性能监控和分析
    • 使用工具进行性能监控和分析,例如 Chrome DevTools、Vue DevTools 等,及时发现和解决性能问题
  12. 使用 Object.freeze() 进行不需要进行响应式的数据进行优化(vue2)

Vuex

Vuex有几个很重要的概念:

  • State
    • 存储应用状态的地方,是响应式的,即当 State 发生变化时,与之相关的组件将自动更新
  • Getter
    • 类似于计算属性
  • Mutation
    • 是用于变更状态的唯一途径
    • 每个 Mutation 都有一个字符串的事件类型(type)和一个回调函数,用于实际的状态变更
  • Action
    • Action 提交的是 Mutation,而不是直接变更状态
    • Action 可以包含任意异步操作
  • Module
    • 将 Vuex 的状态划分为模块,每个模块都有自己的 State、Getter、Mutation 和 Action

Mutation 和 Action 的区别

特点 Mutation Action
同步/异步 同步 异步
直接/间接 直接修改状态 通过提交 Mutation 间接修改状态

为什么Mutation是同步,Action是异步?

  1. Vue2的响应性不完整,不能监听数组的变化、对象属性的变化,
  2. 上面也有说过,Action是调用Mutation间接修改状态,如果Mutation是异步的那么会导致一个问题就是我们知道Mutation是何时调用的,却不知道State的值是何时被需修改的。(这也主要是因为Vue2的响应式有些缺陷导致的)
  3. 同步变化,vuex 内部调用 Mutation 之前记录状态,然后调用 Mutation ,然后获得修改后的状态。

Vue-router

这个比较简单,我们需要记住几个东西:

  1. hash模式
  2. history模式

对比

hash history
表现形式 http://aaa/#/user/id http://aaa/user/id
基于api onhashchange 配合 history.pushState + window.addEventListener("popstate", ()=> {})
配置方式 不需要后端 需要后端协助

这里是手写了一个简单的实现机制

相关推荐
正小安25 分钟前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch2 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光2 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   2 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   2 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web2 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常2 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇3 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho4 小时前
【TypeScript】知识点梳理(三)
前端·typescript