diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 8117d68d7c6..ff3533bd55f 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -337,7 +337,9 @@ function setupStatefulComponent( if (isPromise(setupResult)) { if (__SSR__) { // return the promise so server-renderer can wait on it - return setupResult + return setupResult.then(resolvedResult => { + handleSetupResult(instance, resolvedResult, parentSuspense) + }) } else if (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry. diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index e78099a6703..19034429f6f 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -7,17 +7,21 @@ import { VNode, createVNode } from 'vue' -import { isString } from '@vue/shared' +import { isString, isPromise, isArray } from '@vue/shared' type SSRBuffer = SSRBufferItem[] -type SSRBufferItem = string | Promise +type SSRBufferItem = string | ResolvedSSRBuffer | Promise type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[] -function createSSRBuffer() { +function createBuffer() { let appendable = false + let hasAsync = false const buffer: SSRBuffer = [] return { buffer, + hasAsync() { + return hasAsync + }, push(item: SSRBufferItem) { const isStringItem = isString(item) if (appendable && isStringItem) { @@ -26,18 +30,14 @@ function createSSRBuffer() { buffer.push(item) } appendable = isStringItem + if (!isStringItem && !isArray(item)) { + // promise + hasAsync = true + } } } } -export async function renderToString(app: App): Promise { - const resolvedBuffer = (await renderComponent( - app._component, - app._props - )) as ResolvedSSRBuffer - return unrollBuffer(resolvedBuffer) -} - function unrollBuffer(buffer: ResolvedSSRBuffer): string { let ret = '' for (let i = 0; i < buffer.length; i++) { @@ -51,20 +51,35 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string { return ret } -export async function renderComponent( +export async function renderToString(app: App): Promise { + const resolvedBuffer = (await renderComponent( + app._component, + app._props + )) as ResolvedSSRBuffer + return unrollBuffer(resolvedBuffer) +} + +export function renderComponent( comp: Component, props: Record | null = null, children: VNode['children'] = null, parentComponent: ComponentInternalInstance | null = null -): Promise { - // 1. create component buffer - const { buffer, push } = createSSRBuffer() - - // 2. create actual instance +): ResolvedSSRBuffer | Promise { const vnode = createVNode(comp, props, children) const instance = createComponentInstance(vnode, parentComponent) - await setupComponent(instance, null) + const res = setupComponent(instance, null) + if (isPromise(res)) { + return res.then(() => innerRenderComponent(comp, instance)) + } else { + return innerRenderComponent(comp, instance) + } +} +function innerRenderComponent( + comp: Component, + instance: ComponentInternalInstance +): ResolvedSSRBuffer | Promise { + const { buffer, push, hasAsync } = createBuffer() if (typeof comp === 'function') { // TODO FunctionalComponent } else { @@ -77,7 +92,11 @@ export async function renderComponent( // TODO warn component missing render function } } - // TS can't figure this out due to recursive occurance of Promise in type - // @ts-ignore - return Promise.all(buffer) + // If the current component's buffer contains any Promise from async children, + // then it must return a Promise too. Otherwise this is a component that + // contains only sync children so we can avoid the async book-keeping overhead. + return hasAsync() + ? // TS can't figure out the typing due to recursive appearance of Promise + Promise.all(buffer as any) + : (buffer as ResolvedSSRBuffer) }