Jw
首页
技术
英语
首页
技术
英语
  • 01-权衡的艺术

    • 1.1 命令式与声明式
    • 1.2 性能与可维护性的权衡
    • 1.3 虚拟DOM的性能到底如何
    • 1.4 运行时和编译时
  • 02-框架设计的核心要素

    • 2.1 提升用户的开发体验
    • 2.2 控制框架代码的体积
    • 2.3 框架要做到良好的 Tree-Shaking
    • 2.4 框架应该输出怎样的构建产物
    • 2.5 特性开关
    • 2.6 错误处理
    • 2.7 良好的 TypeScript 类型支持
  • 03-Vue.js 3 的设计思路

    • 3.1 声明式地描述UI
    • 3.2 初识渲染器
    • 3.3 组件的本质
    • 3.4 模板的工作原理
    • 3.5 Vue.js 是各个模块组成的有机整体
  • 04-响应系统的作用与实现

    • 4.1 响应式数据与副作用函数
    • 4.2 响应式数据的基本实现
    • 4.3 设计一个完善的响应系统
    • 4.4 分支切换与 cleanup
    • 4.5 嵌套的effect与effect栈
    • 4.6 避免无限递归循环
    • 4.7 调度执行
    • 4.8 计算属性 computed 与 lazy
    • 4.9 watch 的实现原理
    • 4.10 立即执行的 watch 与回调执行时机
    • 4.11 过期的副作用
  • 05-非原始值的响应式方案

    • 5.1 理解 Proxy 和 Reflect
    • 5.2 JavaScript 对象及 Proxy 的工作原理
    • 5.3 如何代理 Object
    • 5.4 合理地触发响应
    • 5.5 浅响应与深响应
    • 5.6 只读和浅只读
    • 5.7 代理数组
    • 5.7.1 数组的索引与 length
    • 5.7.2 遍历数组
    • 5.7.3 数组的查找方法
    • 5.7.4 隐式修改数组长度的原型方法
    • 5.8 代理 Set 和 Map
    • 5.8.1 如何代理Set和Map
    • 5.8.2 建立响应联系
    • 5.8.3 避免污染原始数据
    • 5.8.4 处理forEach
    • 5.8.5 迭代器方法
    • 5.8.6 values与keys方法
  • 06-原始值的响应式方案

    • 6.1 引入 ref 的概念
    • 6.2 响应丢失问题
    • 6.3 自动脱ref
  • 07-渲染器的设计

    • 7.1 渲染器与响应系统的结合
    • 7.2 渲染器的基本概念
    • 7.3 自定义渲染器
  • 08-挂载与更新

    • 8.1 挂载子节点和元素的属性
    • 8.2 HTML Attributes 与 DOM Properties
    • 8.3 正确地设置元素属性
    • 8.4 class的处理
    • 8.5 卸载操作
    • 8.6 区分vnode类型
    • 8.7 事件的处理
    • 8.8 事件冒泡与更新时机问题
    • 8.9 更新子节点
    • 8.10 文本节点和注释节点
    • 8.11 Fragment
  • 09-简单 Diff 算法

    • 9.1 减少 DOM 操作的性能开销
    • 9.2 DOM 复用与 key 的作用
    • 9.3 找到需要移动的元素
    • 9.4 如何移动元素
    • 9.5 添加新元素
    • 9.6 移除不存在的元素
  • 10-双端 Diff 算法

    • 10.1 双端比较的原理
    • 10.3 非理想状况的处理方式
    • 10.4 添加新元素
    • 10.5 移除不存在的元素
  • 11-快速 Diff 算法

    • 11.1 相同的前置元素和后置元素
    • 11.2 判断是否需要进行 DOM 移动操作
    • 11.3 如何移动元素
  • 12-组件的实现原理

    • 12.1 渲染组件
    • 12.2 组件状态与自更新
    • 12.3 组件实例与组件的生命周期
    • 12.4 props与组件的被动更新
    • 12.5 setup 函数的作用与实现
    • 12.6 组件事件与 emit 的实现
    • 12.7 插槽的工作原理与实现
    • 12.8 注册生命周期
  • 13-异步组件与函数式组件

    • 13.1 异步组件要解决的问题
    • 13.2.1 封装defineAsyncComponent函数
    • 13.2.2 超时与 Error 组件
    • 13.2.3 延迟与 Loading 组件
    • 13.2.4 重试机制
    • 13.3 函数式组件
  • 14-内建组件和模块

    • 14.1.1 组件的激活与失活
    • 14.1.2 include 和 exclude
    • 14.1.3 缓存管理
    • 14.2.1 Teleport 组件要解决的问题
    • 14.2.2 实现 Teleport 组件
    • 14.3.1 原生 DOM 的过渡
    • 14.3.2 实现 Transition 组件
  • 15-编译器核心技术概览

    • 15.1 模板 DSL 的编译器
    • 15.2 parser 的实现原理与状态机
    • 15.3 构造AST
    • 15.4 AST 的转换与插件化架构
    • 15.4.1 节点的访问
    • 15.4.2 转换上下文与节点操作
    • 15.4.3 进入与退出
    • 15.5 将模板 AST 转为 JavaScript AST
    • 15.6 代码生成
  • 16-解析器

    • 16.1 文本模式及其对解析器的影响
    • 16.2 递归下降算法构造模板 AST
    • 16.3 状态机的开启与停止
    • 16.4 解析标签节点
    • 16.5 解析属性
    • 16.6.1 解析文本
    • 16.6.2 解码命名字符引用
    • 16.6.3 解码数字字符引用
    • 16.7 解析插值与注释
  • 17-编译优化

    • 17.1.1 传统Diff算法问题
    • 17.1.2 Block 与 PatchFlags
    • 17.1.3 收集动态节点
    • 17.1.4 渲染器的运行时支持
    • 17.2.1 带有 v-if 指令的节点
    • 17.2.2 带有 v-for 指令的节点
    • 17.2.3 Fragment 的稳定性
    • 17.3 静态提升
    • 17.4 预字符串化
    • 17.5 缓存内联事件处理函数
    • 17.6 v-once
  • 18-同构渲染

    • 18.1 CSR、SSR 以及同构渲染
    • 18.2 将虚拟 DOM 渲染为 HTML 字符串
    • 18.3 将组件渲染为 HTML 字符串
    • 18.4 客户端激活的原理
    • 18.5.1 组件的生命周期
    • 18.5.2 使用跨平台的 API
    • 18.5.3 只在某一端引入模块
    • 18.5.4 避免交叉请求引起的状态污染
    • 18.5.5 ClientOnly 组件

14.1.1 组件的激活与失活

KeepAlive 让被切走的组件不被真正销毁:把组件渲染的内容从原容器「搬进」一个隐藏容器,需要时再「搬回来」,对应生命周期钩子 activated / deactivated。

一、KeepAlive 是什么

  • 名字借鉴 HTTP 协议的 KeepAlive(HTTP 持久连接):允许多个请求/响应共用一个 TCP 连接,避免频繁销毁、重建连接带来的额外开销。
  • 组件层面的 KeepAlive 要解决的是同一类问题:组件反复「销毁 + 重新创建」的开销。
  • 实现本质:缓存管理 + 特殊的挂载 / 卸载逻辑。

二、核心思路:假卸载与假挂载

时机普通组件被 KeepAlive 的组件
切走真卸载:移除 DOM、销毁组件实例假卸载:把内容移动到隐藏容器,实例与状态保留
切回真挂载:新建实例、重新创建 DOM假挂载:把内容从隐藏容器移回原容器,复用原实例
生命周期unmounted / mounteddeactivated / activated

为此渲染器要提供两个能力:

  1. move:把一段内容(组件渲染出的 DOM)移动到另一个容器;
  2. createElement:创建一个不插入文档的隐藏容器。

关键:KeepAlive 不直接操作 document,而是借用渲染器注入的 move,所以自定义渲染器同样能生效(见第五节)。

三、渲染器侧的改造

1. 给 KeepAlive 组件实例注入 keepAliveCtx

挂载组件时,如果发现 vnode.type.__isKeepAlive 为真,就往组件实例上挂一个 keepAliveCtx:

// 组件实例
const instance = {
  state,
  props: shallowReactive(props),
  isMounted: false,
  subTree: null,
  slots,
  mounted: [],
  unmounted: [],
  keepAliveCtx: null // 新增:只有 KeepAlive 组件的实例才会有该属性
}

// 检查当前要挂载的组件是否是 KeepAlive 组件
const isKeepAlive = vnode.type.__isKeepAlive
if (isKeepAlive) {
  // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象,由渲染器注入
  instance.keepAliveCtx = {
    // move:把 vnode 所描述的内容移动到指定容器
    move: (vnode, container, anchor) => {
      // 本质上是把组件渲染的内容(子树的真实 DOM)移动过去
      insert(vnode.component.subTree.el, container, anchor)
    },
    createElement
  }
}

2. patch:切回时改走「激活」而不是挂载

// patch 中处理组件类型 vnode 的分支
if (!n1) {
  // 该组件已经被 KeepAlive 过:不重新挂载,而是激活
  if (n2.keptAlive) {
    n2.keepAliveInstance._activate(n2, container, anchor)
  } else {
    mountComponent(n2, container, anchor)
  }
} else {
  patchComponent(n1, n2, container)
}

3. unmount:该缓存的组件不真卸载

function unmount(vnode) {
  if (vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
    return
  }

  if (typeof vnode.type === 'object') {
    // shouldKeepAlive 标识该组件是否应该被 KeepAlive
    if (vnode.shouldKeepAlive) {
      // 不真卸载,交给它的父组件(KeepAlive)使其失活
      vnode.keepAliveInstance._deActivate(vnode)
    } else {
      vnode.component.unmounted.forEach(hook => hook())
      unmount(vnode.component.subTree)
    }
    return
  }

  const parent = vnode.el.parentNode
  if (parent) parent.removeChild(vnode.el)
}

四、KeepAlive 组件实现

const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup(props, { slots }) {
    // 缓存对象:key 为 vnode.type,value 为 vnode
    const cache = new Map()
    // 当前 KeepAlive 组件的实例
    const instance = currentInstance
    // 渲染器注入的 keepAliveCtx:move 用于把一段内容移动到另一个容器
    const { move, createElement } = instance.keepAliveCtx

    // 创建隐藏容器(不挂到文档上)
    const storageContainer = createElement('div')

    // 两个内部函数会被渲染器调用
    instance._deActivate = (vnode) => {
      // 失活:移动到隐藏容器
      move(vnode, storageContainer)
    }
    instance._activate = (vnode, container, anchor) => {
      // 激活:移动回原容器
      move(vnode, container, anchor)
    }

    return () => {
      // 默认插槽的内容就是要被 KeepAlive 的组件
      const rawVNode = slots.default()
      // 非组件的 vnode 无法被 KeepAlive,直接渲染
      if (typeof rawVNode.type !== 'object') return rawVNode

      // 挂载时先查缓存
      const cachedVNode = cache.get(rawVNode.type)
      if (cachedVNode) {
        // 命中缓存 → 不挂载,而是激活
        rawVNode.component = cachedVNode.component // 继承组件实例
        rawVNode.keptAlive = true // 告诉渲染器不要重新挂载
      } else {
        // 未命中 → 写入缓存,下次切回就不用重新挂载了
        cache.set(rawVNode.type, rawVNode)
      }

      // 告诉渲染器:这个组件不该被真卸载
      rawVNode.shouldKeepAlive = true
      // 把 KeepAlive 组件实例也挂到 vnode 上,方便渲染器里访问
      rawVNode.keepAliveInstance = instance

      return rawVNode
    }
  }
}

几个标记的作用:

标记写在哪谁读作用
__isKeepAliveKeepAlive 组件选项渲染器 mountComponent识别 KeepAlive,注入 keepAliveCtx
keptAlive内部组件 vnode渲染器 patch已缓存 → 调 _activate 而非 mountComponent
shouldKeepAlive内部组件 vnode渲染器 unmount不真卸载 → 调 _deActivate
keepAliveInstance内部组件 vnode渲染器 patch / unmount拿到 KeepAlive 实例,调用 _activate / _deActivate

五、activated / deactivated 的调用时机

钩子触发时机说明
activated组件被从隐藏容器移回页面时(即 _activate 中)首次挂载也会触发;组件已在页面上的更新不触发
deactivated组件被移入隐藏容器时(即 _deActivate 中)此后 unmounted 不会触发,因为组件并未真正卸载

即:

instance._activate = (vnode, container, anchor) => {
  move(vnode, container, anchor)          // 先搬回来
  vnode.component.activated.forEach(hook => hook())   // 再触发 activated
}

instance._deActivate = (vnode) => {
  vnode.component.deactivated.forEach(hook => hook()) // 先触发 deactivated
  move(vnode, storageContainer)                       // 再搬走
}

易错点:被 KeepAlive 的组件只会经历 activated / deactivated,不会再触发 mounted / unmounted(除首次挂载那一次)。

补充:官方实现用 vnode 的 shapeFlag 上的 COMPONENT_SHOULD_KEEP_ALIVE 位来做「不该真卸载」的判断,本书为教学简化成 vnode.shouldKeepAlive 布尔属性,语义一致。

六、为什么自定义渲染器也能生效

  • _activate / _deActivate 里没有出现任何 document / DOM API,移动能力来自渲染器注入的 keepAliveCtx.move;
  • move 内部用的是渲染器选项里的 insert,因此换成 canvas、小程序等自定义渲染器时,KeepAlive 无需改动;
  • 隐藏容器也是用渲染器的 createElement 创建的,同样与平台无关。

这就是「内建组件 + 渲染器内部方法注入」的分工:内建组件只写通用逻辑,平台相关能力全部由渲染器提供。

相关

  • 14.1.2 include 和 exclude:让用户用正则显式指定哪些组件该缓存、哪些不该缓存
  • 14.1.3 缓存管理:缓存只增不减怎么办 —— max 阈值与淘汰策略
最近更新: 2026/10/12 00:05
Contributors: liujw
Next
14.1.2 include 和 exclude