Skip to content

前端核心知识点 04

组件实例的生命周期

组件生命周期的宏观流程

一个组件实例的生命周期可以分为三个主要阶段:

  1. 创建(Creation):实例化、设置响应式、编译模板。
  2. 挂载与更新(Mounting & Update):插入 DOM、响应数据变化。
  3. 卸载(Unmounting):从 DOM 移除、清理资源。

生命周期钩子函数

Vue 提供了一系列生命周期钩子,允许你在特定阶段执行代码。

创建阶段

setup()

  • 组合式 API 的入口。
  • 在 beforeCreate 和 created 之间执行。
  • 返回 setup 中定义的响应式状态和方法。

beforeCreate(选项式)

  • 实例初始化后,数据观测 (data observation) 和事件/watcher 配置之前被调用。

created

实例创建完成。 数据、计算属性、方法、watch/event 回调都已设置。 可以访问响应式数据但尚未挂载到 DOM,$el 为 undefined。

挂载阶段

beforeMount

  • 挂载开始前被调用。
  • render 函数首次被调用。
  • 虚拟 DOM 已创建,但尚未生成真实 DOM

mounted

  • 组件已挂载到 DOM。
  • el 属性现在可用。
  • 可以安全地访问和操作 DOM
  • 适合发起网络请求、设置定时器、集成第三方库。

更新阶段

beforeUpdate

  • 响应式数据变化,导致重新渲染之前调用。
  • 虚拟 DOM 即将重新渲染。
  • DOM 仍是旧的状态

updated

  • 组件因响应式数据变化而重新渲染后调用。
  • DOM 已更新
  • 注意:避免在此钩子中修改状态,可能引发无限循环。

卸载阶段

beforeUnmount(原 beforeDestroy)

  • 实例卸载之前调用。
  • 组件实例仍然完全正常。
  • 清理定时器、取消订阅、移除事件监听器。

unmounted(原 destroyed)

  • 组件实例已从 DOM 中移除。
  • 所有指令都被解绑,所有事件监听器被移除,所有子组件实例被卸载。
  • 实例不可用。

KeepAlive 缓存机制

KeepAlive 是 Vue 3 中一个极其重要的内置组件,它通过缓存组件实例来避免重复的销毁与重建,从而保留组件状态、提升性能。 理解其缓存机制、配置选项和生命周期钩子,是构建高性能 Vue 应用的关键。

核心作用:保存组件状态

当一个组件被 KeepAlive 包裹时

  • 首次渲染:正常创建、挂载。
  • 切换离开:不销毁,而是被缓存起来(失活)。
  • 再次进入:不重新创建,而是从缓存中取出并激活。

保留了什么?

组件实例:data、ref、reactive 状态。 DOM 状态:表单输入、滚动位置、动画状态。 子组件树:整个组件子树都被缓存。

配置选项的实现

KeepAlive 支持三个关键属性:includeexcludemax

include 和 exclude

  • 作用:根据组件的 name 决定是否缓存。
  • 类型:字符串、正则、数组。
  • 优先级:exclude 优先于 include。
text
<KeepAlive 
  :include="/^My.*/" 
  :exclude="['Modal']"
>
  <component :is="currentView" />
</KeepAlive>

max

  • 作用:限制缓存的最大数量,防止内存泄漏。
  • 实现:LRU 策略。
  • LRU 更新:每次组件被激活(onActivated),其 key 会从 keys 中移除并重新添加到末尾,确保“最近使用”的排在后面。

生命周期钩子:onActivated 与 onDeactivated

由于 KeepAlive 组件不会被销毁,mounted/unmounted 钩子只在首次和最终销毁时触发。为此,Vue 提供了两个专用钩子。

onActivated()

触发时机

  • 组件从缓存中被取出并重新插入 DOM 时。
  • 首次 mounted 之后也会触发一次。

用途

  • 启动定时器、动画。
  • 恢复事件监听。
  • 触发数据更新(如轮询)。
text
import { onActivated, onDeactivated } from 'vue';

onActivated(() => {
  console.log('组件被激活');
  startTimer();
});

onDeactivated()

触发时机

  • 组件被切换离开,放入缓存时。

用途

  • 清理定时器、取消订阅,避免内存泄漏。
  • 暂停动画、音乐。
js
onDeactivated(() => {
  console.log('组件被缓存(失活)');
  clearInterval(timer);
  // 不要调用 unmount,KeepAlive 会处理
});

钩子执行顺序

text
// 首次进入
created → mounted → onActivated

// 切换离开
onDeactivated

// 再次进入
onActivated

// 最终销毁(如被 exclude 或 max 淘汰)
onDeactivated → beforeUnmount → unmounted

vue 插槽机制

插槽的基本概念与使用

插槽是组件接口的一部分,允许父组件向子组件插入内容。最基本的用法是默认插槽:

vue
<!-- 子组件 BaseLayout.vue -->
<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
vue
<!-- 父组件使用 -->
<template>
  <BaseLayout>
    <template #header>
      <h1>这里是标题</h1>
    </template>
    
    <p>这是主要内容</p>
    
    <template #footer>
      <p>版权所有</p>
    </template>
  </BaseLayout>
</template>

动态插槽名与插槽的高级用法

text
<template>
  <MyComponent>
    <template #[dynamicSlotName]="slotProps">
      {{ slotProps.text }}
    </template>
  </MyComponent>
</template>

编译器会将动态插槽名编译为动态属性:

js
function render() {
  return h(MyComponent, null, {
    [this.dynamicSlotName]: (slotProps) => h('span', slotProps.text)
  });
}

插槽与组件生命周期

插槽内容的渲染时机与组件的生命周期密切相关。插槽函数在子组件渲染时执行,这意味着:

  1. 插槽可以访问子组件的当前状态
  2. 插槽内容的更新由子组件的响应式系统驱动
  3. 插槽函数每次渲染时都会重新执行
js
// 子组件中插槽的渲染过程
function render() {
  return h('div', [
    // 渲染默认插槽
    renderSlot(this.$slots, 'default', { 
      // 传递给作用域插槽的数据
      data: this.localData 
    }),
    
    // 渲染具名插槽
    renderSlot(this.$slots, 'header')
  ]);
}

性能优化与注意事项

Vue 在插槽处理上做了多项优化:

  1. 插槽函数缓存:对于非动态插槽,Vue 会缓存插槽函数以避免重复创建
  2. 编译时优化:编译器会标记插槽的类型,运行时根据标记采用不同的处理策略
  3. 响应式追踪:插槽函数内部的响应式依赖会被正确追踪

需要注意的几点:

  • 插槽内容在父组件的作用域中定义,但在子组件的上下文中渲染
  • 作用域插槽通过函数参数传递数据,实现了父子组件间的安全数据流
  • 插槽函数每次执行都会创建新的 VNode,需要注意性能影响

effectScope

effectScope 的基本概念与使用

在 Vue 3 的响应式系统中,effectScope 是一个相对高级但非常实用的 API,它允许开发者更精细地控制响应式副作用(effects)的生命周期。 通过 effectScope,我们可以将一组相关的副作用组织在一起,并能够一次性停止它们,这对于构建复杂的响应式系统和插件非常重要。

js
import { effectScope, ref, watchEffect } from 'vue'

// 创建一个 effect 作用域
const scope = effectScope()

// 在作用域内运行副作用
scope.run(() => {
  const count = ref(0)
  
  watchEffect(() => {
    console.log(`Count: ${count.value}`)
  })
  
  setInterval(() => {
    count.value++
  }, 1000)
})

// 在适当的时候停止所有副作用
scope.stop()

effectScope 的工作机制

effectScope 的实现基于 Vue 的响应式系统内部机制。每个 effectScope 实例本质上是一个副作用收集器,它会跟踪在其内部创建的所有副作用

effectScope 与组件生命周期

在 Vue 组件中,每个组件实例都会创建自己的 effectScope,用于收集该组件的所有副作用 当组件被卸载时,其对应的 effectScope 会被自动停止,从而清理所有副作用。 这种方式确保了组件的副作用能够随着组件的生命周期自动管理,避免了内存泄漏。

独立作用域与嵌套作用域

effectScope 支持创建独立作用域和嵌套作用域

effectScope 在实际应用中的使用场景

  • 插件开发
  • 可组合函数(Composables)
  • 复杂的状态管理

总结

effectScope 是 Vue 3 响应式系统中一个强大的机制,它提供了一种精细化管理副作用生命周期的方式:

  • 作用域管理:通过 effectScope 可以将相关的副作用组织在一起
  • 生命周期控制:通过 scope.stop() 可以一次性停止作用域内的所有副作用
  • 内存管理:合理的使用 effectScope 可以避免内存泄漏
  • 插件友好:为插件和可组合函数提供了更好的副作用管理方式

effectScope 的引入完善了 Vue 的响应式系统,使得开发者能够更精确地控制副作用的创建和销毁,特别是在构建复杂应用、插件或可组合函数时, 它是一个非常有价值的工具。

v-memo 的基本概念与使用

v-memo 是 Vue 3.2 引入的一个编译期优化指令,它接受一个数组作为参数,数组中的每一项都代表一个依赖值。 当这些依赖值都没有发生变化时,Vue 会跳过该元素及其所有子元素的更新。

vue
<template>
  <div v-memo="[valueA, valueB]">
    <ExpensiveComponent :data="complexData" />
    <div>
      <p>{{ valueA }}</p>
      <p>{{ valueB }}</p>
    </div>
  </div>
</template>

在上面的例子中,只有当 valueA 或 valueB 发生变化时,整个 <div> 及其子元素才会重新渲染。 如果这两个值都没有变化,即使 complexData 发生了变化,也会跳过更新。

v-memov-for 的结合使用

v-memo 最强大的场景之一是与 v-for 结合使用,用于优化大型列表的渲染:

vue
<template>
  <div>
    <div 
      v-for="item in list" 
      :key="item.id"
      v-memo="[item.id, item.value]"
    >
      <ExpensiveListItem :item="item" />
      <div class="item-details">
        <p>ID: {{ item.id }}</p>
        <p>Value: {{ item.value }}</p>
        <p>Last updated: {{ item.lastUpdated }}</p>
      </div>
    </div>
  </div>
</template>

在这个例子中,对于列表中的每一项,只有当 item.iditem.value 发生变化时,该项才会重新渲染。 即使 item.lastUpdated 或其他属性发生变化,也会跳过更新。

编译后,Vue 会为每个列表项生成独立的 memo 缓存:

js
// 编译后的简化代码
function render() {
  return renderList(this.list, (item, index) => {
    return withMemo(
      [item.id, item.value],
      () => {
        return h('div', { key: item.id }, [
          h(ExpensiveListItem, { item }),
          h('div', { className: 'item-details' }, [
            h('p', `ID: ${item.id}`),
            h('p', `Value: ${item.value}`),
            h('p', `Last updated: ${item.lastUpdated}`)
          ])
        ])
      },
      this,
      index // 每个列表项有唯一的索引
    )
  })
}

v-memo 的运行时处理

在 Vue 的运行时中,v-memo 的处理涉及到组件更新过程中的优化判断:

js
// 简化的 patch 过程
function patch(oldVNode, newVNode, container) {
  // 检查是否是 memo 组件
  if (newVNode.memo) {
    // 检查依赖是否发生变化
    if (!isMemoDirty(oldVNode.memoData, newVNode.memo)) {
      // 依赖未变化,直接复用旧的 vnode
      newVNode.el = oldVNode.el
      newVNode.component = oldVNode.component
      return
    }
  }
  
  // 正常的 patch 流程
  // ...
}

当 Vue 检测到一个带有 v-memo 的节点时,会首先检查其依赖是否发生变化。如果没有变化,就会跳过整个子树的更新过程,直接复用之前的 DOM 节点。

v-memo 的高级使用场景

  • 优化复杂组件树
  • 与计算属性结合使用
  • 在动态组件中的使用

性能考量与注意事项

虽然 v-memo 是一个强大的优化工具,但使用时需要注意以下几点:

  1. 依赖数组的正确性:依赖数组必须包含所有可能影响子树渲染的响应式数据。遗漏依赖会导致界面不更新,而包含过多依赖会降低优化效果。

  2. 内存开销:每个使用 v-memo 的元素都会创建缓存,对于大型列表可能会增加内存使用。

  3. 适用场景:v-memo 主要适用于以下场景:

  • 昂贵的组件树渲染
  • 大型列表的优化
  • 频繁更新但只有部分数据变化的场景