Vue3 源码系列 (九):异步组件 defineAsyncComponent 与 Suspense
前面几篇走完了createApp
的流程,理清了diff
算法的思路。现在回归到运行时的核心API
上。在第一篇和第二篇中有解读过watch
和computed
,而本文则主要梳理异步组件的API
。
一、defineAsyncComponent
用于定义异步组件。
1. defineAsyncComponent
入参source
,可以是一个异步函数loader
,也可以是一个包含有异步函数loader
的对象options
。当source
为options
时可以进行更细致的自定义,如推迟时间、异常处理、异常兜底组件、加载组件等。由于import()
动态加载得到的是一个Promise
,因此,loader
常用来结合import()
引入单文件组件来构成异步组件。
1 | export interface AsyncComponentOptions<T = any> { |
函数中主要是定义一个load
函数,通过对结构source
得到的loader
进行异常处理(是否重试),对加载成功的结果进行校验并得到正常的加载结果。load
函数在返回结果中的setup
里调用。
1 | // defineComponent |
defineAsyncComponent
的返回值是个经过defineComponent
处理过的options
。而options
中的setup
有着异步组件的渲染逻辑。主要是调用load
,通过createInnerComp
来创建加载成功的组件,createVNode
来进行异常和加载中的渲染。
1 | export function defineAsyncComponent< |
2. createInnerComp
当加载成功时,调用createInnerComp
根据加载得到的resolvedComp
来创建内部组件。实际上还是createVNode
来创建的。这里继承了外部组件的ref
。
1 | function createInnerComp( |
二、Suspense
在vue3.2
中引入的新特性之一,便是异步组件Suspense
。
1. process
和KeepAlive
类型,Suspense
暴露一个类似组件的API
。当检查到__isSuspense == true
时,判定当前组件为<Suspense>
,会调用process
方法并被传入renderer
内部进行渲染。process
也会根据旧节点是否存在,选择 挂载 或者 对比新旧节点并更新。
1 | // props |
1.1 mountSuspense
首次加载组件时会进入挂载逻辑。通过mountSuspense
来挂载异步组件。
1 | function mountSuspense( |
1.2 patchSuspense
patchSuspense
进行新旧节点的对比与更新。Suspense
有多个分支如活跃、等待、降级等。
- 当前(旧节点)有等待分支
pendingBranch
,根据新旧等待分支节点类型是否相同分别做处理; - 当前(旧节点)没有等待分支
pendingBranch
,根据新的等待分支与当前(旧的)活跃分支节点类型是否相同分别进行处理。
同时也会考虑异步依赖和是否已经处于降级状态。
1 | function patchSuspense( |
1.3 setActiveBranch
setActiveBranch
用来设置活跃分支。
1 | function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) { |
2. SuspenseBoundary
2.1 ts
类型
1 | export interface SuspenseBoundary { |
2.2 createSuspenseBoundary
使用createSuspenseBoundary
来生成一个SuspenseBoundary
类型的suspense
。由上面的ts
类型我们知道,这个suspense
具有resolve
, fallback
, move
, next
, registerDep
, unmount
等方法。下面就一个个分析每个方法的主要功能。
2.2.1 suspense.resolve
在拿到期望的异步结果时,调用resolve
来用等待分支替换掉当前的活跃分支,从而渲染期望的内容。
- 因此,首先就是要保证异步组件具有等待分支,且组件尚未被卸载(开发环境);
- 之后调用从
suspenseInternals
中拿到的move
函数将等待分支从离线容器移动到实际容器中,并将其设置为活跃分支; - 沿着
suspense.parent
链向上查找,将所有副作用合并到最外层的未解决的Suspense
中; - 如果向上查找时没有发现未解决的先代
Suspense
,则处理当前Suspense
的所有副作用,并将副作用列表清空; - 触发
Suspense
的onResolve
事件。
1 | const suspense: SuspenseBoundary = { |
2.2.2 suspense.fallback
fallback
用于挂载降级内容Fallback
。
- 触发
onFallback
事件; - 有延迟动画
transition
则在其afterLeave
动画钩子中挂载Fallback
内容; - 卸载当前活跃分支;
- 若没有延迟动画则直接挂载
Fallback
内容。
1 | const suspense: SuspenseBoundary = { |
2.2.3 suspense.move
处理活跃分支和容器。
1 | // function createSuspenseBoundary |
2.2.4 suspense.next
next()
递归取到supense
的活跃分支链的末端。
1 | const suspense: SuspenseBoundary = { |
2.2.5 suspense.registerDep
registerDep
用于注册依赖,接收一个实例instance
和渲染副作用函数setupRenderEffect
。若注册时,suspense
处于等待期,则其异步依赖数量+1
;随后注册实例instance
上的异步依赖asyncDep
,得到一个Promise
。在then
中使用handleSetupResult
来处理异步依赖的解决结果,并执行渲染副作用setupRenderEffect
;如果suspense
处于等待期,则将其deps
数量-1
,因为一开始+1
。
1 | const suspense: SuspenseBoundary = { |
2.2.6 unmount
卸载suspense
:将suspense.isUnmounted
置为true
,卸载活跃分支和等待分支。
1 | const suspense: SuspenseBoundary = { |