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组件通信方式 | 只能单向,父子组件间才有效 |
动态组件
通过使用并动态绑定is属性,可以实现动态切换多个组件的功能。
// 组件对象
const Foo = { /* ... */ }
const Bar = { /* ... */ }
// 动态组件
<component :is="currentComponent"/>
data() {
return {
currentComponent: 'Foo'
}
}
异步组件
异步组件通过定义一个返回Promise的工厂函数,实现组件的异步加载。
const AsyncComponent = () => ({
// 组件加载中
component: import('./MyComponent.vue'),
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间
delay: 200,
// 加载组件时的提示
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>
实现机制
- keep-alive组件会在内部维护一个对象
-
- cache:用来缓存已经创建的组件实例
- 在组件切换时,优先获取include内的组件,过滤exclude内的组件,然后再检查缓存中是否已经有实例
-
- 如果有则取出重用
- 如果没有缓存,则正常创建新实例,并存储到缓存中。
- 在组件销毁时,不会立即执行销毁,而是将其保存在缓存中(也要判断include和exclude)
- keep-alive 会拦截组件的钩子函数;在适当时机调用 activated 和 deactivated 钩子
- 当缓存数量超过上限时,会释放最近最久未使用的缓存实例
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 的 出口会带有隐含的名字“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>
插槽是如何渲染的呢?
- 编译阶段
-
- 子组件模板中会生成一个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
<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 实现原理
可以简述如下:
- nextTick接收一个回调函数作为参数
- 内部会维护一个异步回调队列数组
- 将传入的回调推入这个异步队列
- 在微任务(promise.then/MutationObserver)空闲时刻执行队列中的回调
- 达成在DOM更新后执行回调的效果
写个例子
let callbacks = [] // 异步回调队列
function nextTick(cb) {
callbacks.push(cb) // 推入回调队列
// 微任务执行callbacks
Promise.resolve().then(flushCallbacks)
}
function flushCallbacks() {
callbacks.forEach(cb => cb()) // 执行队列回调
callbacks = [] // 重置队列
}