vue进阶总结1

23 阅读6分钟

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 在实例被创建之后,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>

实现机制

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

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>

插槽是如何渲染的呢?

  1. 编译阶段
    • 子组件模板中会生成一个Slot AST节点
    • 父组件v-slot会生成Template AST节点
    • 两者都会标注slot名称,建立关联
  1. 渲染阶段
    • Vue组件的_render方法会先执行子组件的render
    • render里遇到slot标记会生成comment节点占位
    • 然后执行scoped slot的render,生成父组件传递的slot内容
    • 最后在_update方法Patch时,找到对应评论节点插入内容
  1. 核心流程
    • 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 = [] // 重置队列
}