【知识总结】前端面试前Vue复习资料

大家好,我是Fine。

今天为大家分享一篇Vue专题,面试前你需要知道的Vue相关的知识点,全在这里,一网打尽。

以下是正文:

Vue 生命周期

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

Vue2选项式Vue3选项式Vue3组合式作用
beforeCreatebeforeCreatesetup()在实例初始化之后,数据观测和事件配置之前被调用
createdcreatedonBeforeMount在实例创建完成后被立即调用,可在此执行数据初始化
beforeMountbeforeMountonBeforeMount在挂载开始之前被调用,相关render函数首次被调用
mountedmountedonMounted组件挂载到DOM后调用,可获取DOM节点
beforeUpdatebeforeUpdateonBeforeUpdate数据更新时,虚拟DOM重新渲染和打补丁之前调用
updatedupdatedonUpdated组件DOM更新后调用,可执行依赖于DOM的操作
activatedactivatedonActivatedkeep-alive组件激活时调用
deactivateddeactivatedonDeactivatedkeep-alive组件停用时调用
beforeDestroybeforeUnmountonBeforeUnmount实例销毁之前调用,可执行清理操作
destroyedunmountedonUnmounted实例销毁后调用,调用后实例指向销毁,所有东西可回收
errorCapturederrorCapturedonErrorCaptured捕获子孙组件错误时被调用

为什么 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属性,可以实现动态切换多个组件的功能。

// 组件对象
const Foo = { /* ... */ } 
const Bar = { /* ... */ }

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

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

异步组件

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

const AsyncComponent = () => ({
  // 组件加载中         
  componentimport('./MyComponent.vue'),
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间
  delay200,
  // 加载组件时的提示
  loading: LoadingComponent,
})

然后在组件中使用:

<async-component></async-component>

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

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

keep-alive

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

<!-- 使用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:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

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

使用:

<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:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>

</span>

使用:

<current-user>
  <template v-slot:default="{ user }">
    {{ user.firstName }}
  </template>

</current-user>

动态插槽

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

<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

<div>{{ title }}</div>

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更新后执行回调的效果

写个例子

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运行时和构建时编译

易于理解

ReactJSX (JavaScript XML)编译为 JavaScript中等

嵌入 JavaScript 中

Angular复杂,基于 HTML预编译 (Ahead of Time)较高

双向数据绑定

Vue diff 算法的过程

Vue2

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

  • 深度优先+双指针(头尾交叉对比)的 diff
    • 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,
    • 这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom
    • 之后的节点对比就是深度优先对比的步骤
    • 逐层比较新旧虚拟 DOM 树的节点,对于每一层,它会按照顺序比较节点,找出差异,并标记需要更新的地方。这样的遍历方式有助于更快地发现差异,因为它会首先比较同一层级的节点,然后再递归到下一层级。
    • 通过虚拟节点的key和tag来进行判断是否相同节点
    • 如果相同则将旧节点关联的真实dom的挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组
    • 如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom
    • 深度优先(同层比较):

    • 双指针:

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

流程图

Vu3

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

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

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

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

Vue-router

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

  1. hash模式
  2. history模式

对比


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

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

原文地址: https://juejin.cn/post/7306018817687109684

原文作者: 是洋柿子啊

最后