Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,19 @@ export class ReactiveEffect<T = any>
onTrigger?: (event: DebuggerEvent) => void

constructor(public fn: () => T) {
if (activeEffectScope && activeEffectScope.active) {
activeEffectScope.effects.push(this)
if (activeEffectScope) {
if (activeEffectScope.active) {
activeEffectScope.effects.push(this)
} else {
// The active scope has already been stopped. This happens when a
// component's setup is resumed after a top-level `await` (via the
// compiler-emitted `__restore()` from `withAsyncContext`) but the
// component was unmounted while pending under <Suspense>. Without
// this guard the effect would become an orphan: not held by any
// scope (so it cannot be stopped via the scope chain) yet still
// able to subscribe to reactive deps and fire forever.
this.flags &= ~EffectFlags.ACTIVE
}
}
}

Expand Down
20 changes: 14 additions & 6 deletions packages/reactivity/src/effectScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class EffectScope {
cleanups: (() => void)[] = []

private _isPaused = false
private _warnOnRun = true

/**
* only assigned by undetached scope
Expand All @@ -44,12 +45,19 @@ export class EffectScope {
// TODO isolatedDeclarations ReactiveFlags.SKIP

constructor(public detached = false) {
this.parent = activeEffectScope
if (!detached && activeEffectScope) {
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this,
) - 1
if (activeEffectScope.active) {
this.parent = activeEffectScope
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this,
) - 1
} else {
// The parent scope has already stopped, so this child must not become
// a detached live scope.
this._active = false
this._warnOnRun = false
}
}
}

Expand Down Expand Up @@ -101,7 +109,7 @@ export class EffectScope {
} finally {
activeEffectScope = currentEffectScope
}
} else if (__DEV__) {
} else if (__DEV__ && this._warnOnRun) {
warn(`cannot run an inactive effect scope.`)
}
}
Expand Down
216 changes: 216 additions & 0 deletions packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createBlock,
createCommentVNode,
createElementBlock,
getCurrentInstance,
h,
nextTick,
nodeOps,
Expand All @@ -29,13 +30,15 @@ import {
shallowRef,
watch,
watchEffect,
withAsyncContext,
withDirectives,
} from '@vue/runtime-test'
import {
computed,
createApp,
defineAsyncComponent as defineAsyncComp,
defineComponent,
effectScope,
inject,
provide,
} from 'vue'
Expand Down Expand Up @@ -3009,4 +3012,217 @@ describe('Suspense', () => {
expect(unmounted).toHaveBeenCalledTimes(1)
})
})

describe('async setup with top-level await', () => {
test('pending branch replaced before async setup resolves', async () => {
const updateSpy = vi.fn()
const stateRef = ref(0)
let resolvePending!: (v?: unknown) => void

const SlowComp = defineComponent({
async setup() {
let __temp: any, __restore: any
;[__temp, __restore] = withAsyncContext(
() =>
new Promise(r => {
resolvePending = r
}),
)
__temp = await __temp
__restore()

watch(stateRef, updateSpy)

return () => h('div', 'slow')
},
})

const FillerComp = defineComponent({
setup: () => () => h('div', 'filler'),
})

const view = shallowRef<any>(SlowComp)
const Comp = defineComponent({
setup: () => () =>
h(Suspense, null, {
default: h(view.value),
fallback: h('div', 'fallback'),
}),
})

const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)

view.value = FillerComp
await nextTick()
expect(serializeInner(root)).toBe(`<div>filler</div>`)

// wait a macro task tick for all micro ticks to resolve
resolvePending(undefined)
await new Promise(r => setTimeout(r))

stateRef.value++
await nextTick()
expect(updateSpy).not.toHaveBeenCalled()
})

test('boundary unmounted before async setup resolves', async () => {
const updateSpy = vi.fn()
const stateRef = ref(0)
let resolvePending!: (v?: unknown) => void

const SlowComp = defineComponent({
async setup() {
let __temp: any, __restore: any
;[__temp, __restore] = withAsyncContext(
() =>
new Promise(r => {
resolvePending = r
}),
)
__temp = await __temp
__restore()

watch(stateRef, updateSpy)

return () => h('div', 'slow')
},
})

const root = nodeOps.createElement('div')
render(
h(() =>
h(Suspense, null, {
default: h(SlowComp),
fallback: h('div', 'fallback'),
}),
),
root,
)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)

render(null, root)

resolvePending(undefined)
await new Promise(r => setTimeout(r))

stateRef.value++
await nextTick()
expect(updateSpy).not.toHaveBeenCalled()
})

test('repeated branch replacement before async setup resolves', async () => {
const updateSpy = vi.fn()
const stateRef = ref(0)
const resolvers: Array<(v?: unknown) => void> = []

const SlowComp = defineComponent({
async setup() {
let __temp: any, __restore: any
;[__temp, __restore] = withAsyncContext(
() =>
new Promise(r => {
resolvers.push(r)
}),
)
__temp = await __temp
__restore()

const uid = getCurrentInstance()!.uid
watch(stateRef, () => {
updateSpy(uid)
})

return () => h('div', 'slow')
},
})

const FillerComp = defineComponent({
setup: () => () => h('div', 'filler'),
})

const view = shallowRef<any>(FillerComp)
const Comp = defineComponent({
setup: () => () =>
h(Suspense, null, {
default: h(view.value),
fallback: h('div', 'fallback'),
}),
})

const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>filler</div>`)

for (let i = 0; i < 3; i++) {
view.value = SlowComp
await nextTick()
view.value = FillerComp
await nextTick()
}

expect(resolvers.length).toBe(3)
resolvers.forEach(r => r(undefined))
await new Promise(r => setTimeout(r))

stateRef.value++
await nextTick()
expect(updateSpy).not.toHaveBeenCalled()
})

test('nested scope created after pending branch is abandoned', async () => {
const updateSpy = vi.fn()
const stateRef = ref(0)
let resolvePending!: (v?: unknown) => void

const SlowComp = defineComponent({
async setup() {
let __temp: any, __restore: any
;[__temp, __restore] = withAsyncContext(
() =>
new Promise(r => {
resolvePending = r
}),
)
__temp = await __temp
__restore()

effectScope().run(() => {
watch(stateRef, updateSpy)
})

return () => h('div', 'slow')
},
})

const FillerComp = defineComponent({
setup: () => () => h('div', 'filler'),
})

const view = shallowRef<any>(SlowComp)
const Comp = defineComponent({
setup: () => () =>
h(Suspense, null, {
default: h(view.value),
fallback: h('div', 'fallback'),
}),
})

const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)

view.value = FillerComp
await nextTick()
expect(serializeInner(root)).toBe(`<div>filler</div>`)

resolvePending(undefined)
await new Promise(r => setTimeout(r))

stateRef.value++
await nextTick()
expect(updateSpy).not.toHaveBeenCalled()
})
})
})
Loading