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在实例被创建之后,data和methods还未初始化之前调用setup在组件创建之后,data和methods初始化之前被调用
所以 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>
实现机制
- keep-alive组件会在内部维护一个对象
- cache:用来缓存已经创建的组件实例
- 在组件切换时,优先获取include内的组件,过滤exclude内的组件,然后再检查缓存中是否已经有实例
- 如果有则取出重用
- 如果没有缓存,则正常创建新实例,并存储到缓存中。
- 在组件销毁时,不会立即执行销毁,而是将其保存在缓存中(也要判断include和exclude)
- keep-alive 会拦截组件的钩子函数;在适当时机调用 activated 和 deactivated 钩子
- 当缓存数量超过上限时,会释放最近最久未使用的缓存实例
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>
插槽是如何渲染的呢?
-
编译阶段
- 子组件模板中
<slot>会生成一个Slot AST节点 - 父组件v-slot会生成Template AST节点
- 两者都会标注slot名称,建立关联
- 子组件模板中
-
渲染阶段
- Vue组件的_render方法会先执行子组件的render
- render里遇到slot标记会生成comment节点占位
- 然后执行scoped slot的render,生成父组件传递的slot内容
- 最后在_update方法Patch时,找到对应评论节点插入内容
-
核心流程
- 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 实现原理
可以简述如下:
- nextTick接收一个回调函数作为参数
- 内部会维护一个异步回调队列数组
- 将传入的回调推入这个异步队列
- 在微任务(promise.then/MutationObserver)空闲时刻执行队列中的回调
- 达成在DOM更新后执行回调的效果
写个例子
js
let callbacks = [] // 异步回调队列
function nextTick(cb) {
callbacks.push(cb) // 推入回调队列
// 微任务执行callbacks
Promise.resolve().then(flushCallbacks)
}
function flushCallbacks() {
callbacks.forEach(cb => cb()) // 执行队列回调
callbacks = [] // 重置队列
}
Vue3 ref 和 reactive 区别,如何选择?
个人认为 ref 和 reactive 其实 没必要这个都抛出来给我们用,容易造成一些使用困扰,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
- 逐层比较新旧虚拟 DOM 树的节点,对于每一层,它会按照顺序比较节点,找出差异,并标记需要更新的地方。这样的遍历方式有助于更快地发现差异,因为它会首先比较同一层级的节点,然后再递归到下一层级。
-
双指针:
- 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,
- 这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom
- 之后的节点对比就是深度优先对比的步骤
-
这样一直递归的遍历下去,直到整棵树完成对比。
流程图
Vu3
- 双指针+最长递增子序列 diff算法
- 预处理前前置节点
- 从头对比找到有相同的节点 patch ,发现不同,立即跳出
- 预处理后置节点
- 如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环
- 处理仅有新增节点的情况
- 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。
- 处理仅有删除节点的情况
- 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )
- 处理新增、删除、移动混合的情况
- 把没有比较过的新的vnode节点,建立一个数组,每个子元素都是
0里面的数字记录老节点的索引 ,数组索引就是新节点的索引- map = [0,0,0,0]
- 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex
- 没有找到与老节点对应的新节点,卸载当前老节点
- 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中
- 如果节点发生移动 记录已经移动了
- patch新老节点 找到新的节点进行patch节点
- 把没有比较过的新的vnode节点,建立一个数组,每个子元素都是
- 预处理前前置节点
Vue 常见的性能优化方式(结合项目场景)
在实际项目中,Vue 的性能优化需要根据具体的场景和需求来选择合适的策略。以下是一些常见的 Vue 性能优化方式,结合项目场景进行总结:
- 使用生产环境构建:
- 在生产环境中使用 Vue 的生产版本,以减少体积和提高性能。
- 异步组件和路由懒加载:
- 对于大型项目,使用异步组件和路由懒加载,以分割代码并实现按需加载,减小初始加载体积。
- 合理使用 v-if 和 v-show:
- 对于频繁切换的元素,使用 v-show,对于不经常切换的元素,使用 v-if,以减少 DOM 元素的挂载和卸载。
- 合理使用 v-for:
- 遍历大数据集时,避免在模板中访问复杂度较高的属性,最好在数据源中进行预处理。 如果数据不变,可以考虑使用 Object.freeze 冻结对象,以防止 Vue 的响应式系统监听它。
- 合理使用计算属性和 Watch:
- 将复杂的计算逻辑放入计算属性,避免在模板中进行复杂的计算。 使用 deep 选项和 immediate 选项来优化 Watcher。
- 合理使用事件委托:
- 在父组件上使用事件委托,将事件处理推移到父组件上,以减少子组件的监听器数量。
- 合理使用 keep-alive:
- 对于频繁切换的组件,可以考虑使用
<keep-alive>缓存组件实例,以减少组件的销毁和重新创建。
- 对于频繁切换的组件,可以考虑使用
- 合理使用缓存
- 利用缓存机制,例如在数据请求结果中使用缓存,以避免不必要的重复请求。
- 合理使用过渡效果和动画:
- 控制过渡效果和动画的触发时机,避免在大量元素上使用过渡效果,以提高性能。
- 优化网络请求
- 使用合适的数据加载方式,例如分页加载或滚动加载,以降低页面初始化时的请求量。
- 性能监控和分析
- 使用工具进行性能监控和分析,例如 Chrome DevTools、Vue DevTools 等,及时发现和解决性能问题
- 使用 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是异步?
- Vue2的响应性不完整,不能监听数组的变化、对象属性的变化,
- 上面也有说过,Action是调用Mutation间接修改状态,如果Mutation是异步的那么会导致一个问题就是我们知道Mutation是何时调用的,却不知道State的值是何时被需修改的。(这也主要是因为Vue2的响应式有些缺陷导致的)
- 同步变化,vuex 内部调用 Mutation 之前记录状态,然后调用 Mutation ,然后获得修改后的状态。
Vue-router
这个比较简单,我们需要记住几个东西:
- hash模式
- history模式
对比
| hash | history | |
|---|---|---|
| 表现形式 | http://aaa/#/user/id | http://aaa/user/id |
| 基于api | onhashchange | 配合 history.pushState + window.addEventListener("popstate", ()=> {}) |
| 配置方式 | 不需要后端 | 需要后端协助 |