Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: <Suspense> + <Transition> means mounted() runs too early #5952

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 29 additions & 9 deletions packages/runtime-core/src/apiLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,23 @@ import { DebuggerEvent, pauseTracking, resetTracking } from '@vue/reactivity'

export { onActivated, onDeactivated } from './components/KeepAlive'

export type WRAPPEDHOOK = {
(hook: Function & { __weh?: WRAPPEDHOOK; _isSuspense?: boolean }): void
_isSuspense?: boolean
}

export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
hook: Function & { __weh?: WRAPPEDHOOK; _isSuspense?: boolean },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
): WRAPPEDHOOK | undefined {
if (target) {
const hooks = target[type] || (target[type] = [])
// cache the error handling wrapper for injected hooks so the same hook
// can be properly deduped by the scheduler. "__weh" stands for "with error
// handling".
const wrappedHook =
const wrappedHook: WRAPPEDHOOK =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
Expand All @@ -43,6 +48,15 @@ export function injectHook(
resetTracking()
return res
})
const parent = target.parent
if (
// `<transition>` wrapped in `<suspense>`
target.suspense &&
parent?.type.name === 'BaseTransition' &&
!parent.suspense
) {
wrappedHook._isSuspense = true
}
if (prepend) {
hooks.unshift(wrappedHook)
} else {
Expand All @@ -63,8 +77,13 @@ export function injectHook(
}
}

export interface HOOK {
(e: DebuggerEvent): void
_isSuspense?: boolean
}

export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
<T extends HOOK>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
Expand All @@ -86,11 +105,12 @@ export const onRenderTracked = createHook<DebuggerHook>(
LifecycleHooks.RENDER_TRACKED
)

export type ErrorCapturedHook<TError = unknown> = (
err: TError,
instance: ComponentPublicInstance | null,
info: string
) => boolean | void
export type ErrorCapturedHook<TError = unknown> = {
(err: TError, instance: ComponentPublicInstance | null, info: string):
| boolean
| void
_isSuspense?: boolean
}

export function onErrorCaptured<TError = Error>(
hook: ErrorCapturedHook<TError>,
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/components/KeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
injectHook,
onUnmounted,
onMounted,
onUpdated
onUpdated,
WRAPPEDHOOK
} from '../apiLifecycle'
import {
isString,
Expand Down Expand Up @@ -407,7 +408,7 @@ function registerKeepAliveHook(
}

function injectToKeepAliveRoot(
hook: Function & { __weh?: Function },
hook: Function & { __weh?: WRAPPEDHOOK },
type: LifecycleHooks,
target: ComponentInternalInstance,
keepAliveRoot: ComponentInternalInstance
Expand Down
53 changes: 52 additions & 1 deletion packages/runtime-core/src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { isArray, NOOP } from '@vue/shared'
import { ComponentInternalInstance, getComponentName } from './component'
import { warn } from './warning'
import { WRAPPEDHOOK } from './apiLifecycle'

export interface SchedulerJob extends Function {
id?: number
Expand Down Expand Up @@ -47,6 +48,11 @@ const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0

// only for Suspense when `<transition>` wrapped in `<suspense>`
const pendingSuspenseFlushCbs: SchedulerJob[] = []
let activeSuspenseFlushCbs: SchedulerJob[] | null = null
let suspenseFlushIndex = 0

const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

Expand Down Expand Up @@ -149,6 +155,15 @@ export function queuePostFlushCb(cb: SchedulerJobs) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

export function queueSuspenseFlushCb(cb: SchedulerJobs) {
queueCb(
cb,
activeSuspenseFlushCbs,
pendingSuspenseFlushCbs,
suspenseFlushIndex
)
}

export function flushPreFlushCbs(
seen?: CountMap,
parentJob: SchedulerJob | null = null
Expand Down Expand Up @@ -219,6 +234,36 @@ export function flushPostFlushCbs(seen?: CountMap) {
}
}

export function flushSuspenseFlushCbs(seen?: CountMap) {
if (pendingSuspenseFlushCbs.length) {
const deduped = [...new Set(pendingSuspenseFlushCbs)]
pendingSuspenseFlushCbs.length = 0

activeSuspenseFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}

activeSuspenseFlushCbs.sort((a, b) => getId(a) - getId(b))

for (
suspenseFlushIndex = 0;
suspenseFlushIndex < activeSuspenseFlushCbs.length;
suspenseFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activeSuspenseFlushCbs[suspenseFlushIndex])
) {
continue
}
activeSuspenseFlushCbs[suspenseFlushIndex]()
}
activeSuspenseFlushCbs = null
suspenseFlushIndex = 0
}
}

const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id

Expand Down Expand Up @@ -264,7 +309,13 @@ function flushJobs(seen?: CountMap) {
flushIndex = 0
queue.length = 0

flushPostFlushCbs(seen)
if (!pendingPostFlushCbs.find(n => (n as WRAPPEDHOOK)._isSuspense)) {
flushPostFlushCbs(seen)
flushSuspenseFlushCbs(seen)
} else {
queueSuspenseFlushCb(pendingPostFlushCbs)
pendingPostFlushCbs.length = 0
}

isFlushing = false
currentFlushPromise = null
Expand Down