Vue3 异步组件实现分析
前言
在大型应用中,我们需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现这个功能。本节将分析其内部实现原理。
1. 异步组件的基本使用
typescript
// 基础用法
const AsyncComp = defineAsyncComponent(
() => import("./components/MyComponent.vue")
);
// 带配置的用法
const AsyncCompWithOptions = defineAsyncComponent({
loader: () => import("./components/MyComponent.vue"),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000,
});
2. 核心实现分析
defineAsyncComponent 的实现主要分为三个部分:组件定义、加载控制和状态管理。让我们逐步分析其实现原理:
2.1 组件定义
typescript
export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
// 1. 统一配置格式
if (isFunction(source)) {
source = { loader: source };
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout,
suspensible = true,
onError: userOnError,
} = source;
// 2. 维护组件状态
let pendingRequest: Promise<ConcreteComponent> | null = null;
let resolvedComp: ConcreteComponent | undefined;
// 3. 返回一个包装组件
return {
name: "AsyncComponentWrapper",
__asyncLoader: load,
setup() {
// setup 实现...
},
};
}
这个包装组件是一个特殊的组件定义,它包含:
- __asyncLoader: 用于手动触发加载
- setup 函数: 处理组件的实际加载和渲染逻辑
2.2 加载控制
typescript
// 1. 重试机制
let retries = 0;
const retry = () => {
retries++;
pendingRequest = null;
return load();
};
// 2. 加载函数
const load = (): Promise<ConcreteComponent> => {
let thisRequest: Promise<ConcreteComponent>;
return (
pendingRequest ||
(thisRequest = pendingRequest =
loader()
.catch((err) => {
if (userOnError) {
// 3. 错误处理
return new Promise((resolve, reject) => {
const userRetry = () => resolve(retry());
const userFail = () => reject(err);
userOnError(err, userRetry, userFail, retries + 1);
});
} else {
throw err;
}
})
.then((comp) => {
// 4. 防止重复加载
if (thisRequest !== pendingRequest && pendingRequest) {
return pendingRequest;
}
return comp;
}))
);
};
加载控制的关键点:
- pendingRequest 缓存:避免重复发起加载请求
- 重试机制:支持加载失败后重试
- 错误处理:提供用户自定义的错误处理
- 请求竞态处理:确保使用最新的加载结果
2.3 状态管理与渲染
typescript
setup() {
const instance = currentInstance!;
// 1. 缓存检查
if (resolvedComp) {
return () => createInnerComp(resolvedComp!, instance);
}
// 2. 错误处理
const onError = (err: Error) => {
pendingRequest = null;
handleError(err);
};
// 3. Suspense 集成
if (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) {
return load()
.then((comp) => {
return () => createInnerComp(comp, instance);
})
.catch((err) => {
onError(err);
return () =>
errorComponent
? createVNode(errorComponent, { error: err })
: null;
});
}
// 4. 响应式状态
const loaded = ref(false);
const error = ref();
const delayed = ref(!!delay);
// 5. 延迟处理
if (delay) {
setTimeout(() => {
delayed.value = false;
}, delay);
}
// 6. 超时处理
if (timeout != null) {
setTimeout(() => {
if (!loaded.value && !error.value) {
const err = new Error(
`Async component timed out after ${timeout}ms.`
);
onError(err);
error.value = err;
}
}, timeout);
}
// 7. 开始加载
load()
.then(() => {
loaded.value = true;
})
.catch((err) => {
onError(err);
error.value = err;
});
// 8. 条件渲染
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
}
setup 函数的执行过程:
- 检查缓存:如果组件已加载,直接返回
- 设置错误处理函数
- 处理 Suspense 集成:如果在 Suspense 中使用,返回 Promise
- 创建响应式状态:跟踪加载、错误和延迟状态
- 设置延迟定时器:控制 loading 组件的显示时机
- 设置超时定时器:处理加载超时情况
- 触发组件加载:更新状态并处理错误
- 返回渲染函数:根据不同状态渲染不同内容
这个实现通过状态管理和条件渲染,优雅地处理了异步组件的各种场景:
- 首次加载时显示 loading
- 加载成功时显示实际组件
- 加载失败时显示错误组件
- 支持重试和超时处理
2.4 渲染过程分析
异步组件的渲染是通过一个动态的渲染函数来实现的,这个渲染函数会根据组件的不同状态返回不同的 VNode:
typescript
// 渲染函数的实现
return () => {
if (loaded.value && resolvedComp) {
// 1. 已加载状态:渲染实际组件
// - loaded.value 为 true 表示组件已加载完成
// - resolvedComp 是加载得到的实际组件定义
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
// 2. 错误状态:渲染错误组件
// - error.value 包含具体的错误信息
// - 将错误信息通过 props 传递给错误组件
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
// 3. 加载状态:渲染加载组件
// - delayed.value 控制是否延迟显示
// - 只有在 delay 时间后才显示加载组件
return createVNode(loadingComponent);
}
// 4. 初始状态:不渲染任何内容
return null;
};
让我们看看 createInnerComp 的实现:
typescript
function createInnerComp(
comp: ConcreteComponent,
{ vnode: { ref, props, children } }: ComponentInternalInstance
) {
// 1. 创建包含实际组件的 vnode
const vnode = createVNode(comp, props, children);
// 2. 继承外层异步组件的 ref
vnode.ref = ref;
return vnode;
}
渲染过程的状态转换:
javascript
初始渲染
┌─────────────┐
│ null │ ─── delay 时间后 ──┐
└─────────────┘ ▼
┌─────────────┐
│ Loading │
└─────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Error │ ◄── 重试失败 ── 重试 ── │ AsyncComp │
└─────────────┘ └─────────────┘
渲染函数的特点:
-
响应式驱动:
- 通过 ref 管理组件状态
- 状态变化自动触发重新渲染
-
条件渲染:
- 优先级:实际组件 > 错误组件 > 加载组件
- 通过 delayed.value 控制加载组件的显示时机
-
状态传递:
- 错误信息通过 props 传给错误组件
- 实际组件继承异步组件的 props 和 children
-
引用处理:
- 保持 ref 的正确传递
- 确保组件树的完整性
这种实现方式确保了:
- 渲染过程的平滑过渡
- 良好的用户体验
- 错误处理的可靠性
- 状态管理的响应式
3. 关键特性实现
3.1 延迟加载
typescript
// 延迟显示 loading 组件
if (delay) {
setTimeout(() => {
delayed.value = false;
}, delay);
}
// 渲染时的处理
if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
3.2 超时处理
typescript
if (timeout != null) {
setTimeout(() => {
if (!loaded.value && !error.value) {
const err = new Error(`Async component timed out after ${timeout}ms.`);
onError(err);
error.value = err;
}
}, timeout);
}
3.3 错误重试
typescript
const retry = () => {
retries++;
pendingRequest = null;
return load();
};
// 用户自定义错误处理
if (userOnError) {
return new Promise((resolve, reject) => {
const userRetry = () => resolve(retry());
const userFail = () => reject(err);
userOnError(err, userRetry, userFail, retries + 1);
});
}
4. 使用示例
typescript
// 基础异步组件
const AsyncComponent = defineAsyncComponent(
() => import("./components/MyComponent.vue")
);
// 完整配置示例
const AsyncComponentWithOptions = defineAsyncComponent({
// 加载函数
loader: () => import("./components/MyComponent.vue"),
// 加载中显示的组件
loadingComponent: {
template: "<div>Loading...</div>",
},
// 出错时显示的组件
errorComponent: {
template: "<div>Error!</div>",
},
// 延迟显示加载组件的时间
delay: 200,
// 超时时间
timeout: 3000,
// 错误处理
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
// 重试
retry();
} else {
// 失败
fail();
}
},
});
5. 总结
Vue3 异步组件的实现特点:
-
灵活的配置选项:
- 支持简单函数和详细配置对象
- 提供 loading/error 状态组件
- 可配置延迟和超时时间
-
完善的错误处理:
- 支持自定义错误处理
- 内置重试机制
- 超时自动失败
-
优秀的性能考虑:
- 组件加载结果缓存
- 避免重复加载
- 支持 Suspense 集成
-
用户体验保障:
- 延迟显示 loading 状态
- 加载出错时优雅降级
- 可定制的重试策略
这种实现方式既保证了代码分割的性能优化,又提供了良好的用户体验,是大型应用开发中不可或缺的功能。