Skip to content

Commit 0240e82

Browse files
committed
feat(sfc): auto restore current instance after await statements in async setup()
1 parent fd7fa6f commit 0240e82

File tree

5 files changed

+106
-11
lines changed

5 files changed

+106
-11
lines changed

packages/compiler-sfc/__tests__/compileScript.spec.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -824,37 +824,70 @@ const emit = defineEmits(['a', 'b'])
824824
})
825825

826826
describe('async/await detection', () => {
827-
function assertAwaitDetection(code: string, shouldAsync = true) {
827+
function assertAwaitDetection(
828+
code: string,
829+
expected: string | ((content: string) => boolean),
830+
shouldAsync = true
831+
) {
828832
const { content } = compile(`<script setup>${code}</script>`)
829833
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
834+
if (typeof expected === 'string') {
835+
expect(content).toMatch(expected)
836+
} else {
837+
expect(expected(content)).toBe(true)
838+
}
830839
}
831840

832841
test('expression statement', () => {
833-
assertAwaitDetection(`await foo`)
842+
assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
834843
})
835844

836845
test('variable', () => {
837-
assertAwaitDetection(`const a = 1 + (await foo)`)
846+
assertAwaitDetection(
847+
`const a = 1 + (await foo)`,
848+
`1 + (await _withAsyncContext(foo))`
849+
)
838850
})
839851

840852
test('ref', () => {
841-
assertAwaitDetection(`ref: a = 1 + (await foo)`)
853+
assertAwaitDetection(
854+
`ref: a = 1 + (await foo)`,
855+
`1 + (await _withAsyncContext(foo))`
856+
)
842857
})
843858

844859
test('nested statements', () => {
845-
assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
860+
assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
861+
return (
862+
code.includes(`await _withAsyncContext(foo)`) &&
863+
code.includes(`await _withAsyncContext(bar)`)
864+
)
865+
})
846866
})
847867

848868
test('should ignore await inside functions', () => {
849869
// function declaration
850-
assertAwaitDetection(`async function foo() { await bar }`, false)
870+
assertAwaitDetection(
871+
`async function foo() { await bar }`,
872+
`await bar`,
873+
false
874+
)
851875
// function expression
852-
assertAwaitDetection(`const foo = async () => { await bar }`, false)
876+
assertAwaitDetection(
877+
`const foo = async () => { await bar }`,
878+
`await bar`,
879+
false
880+
)
853881
// object method
854-
assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
882+
assertAwaitDetection(
883+
`const obj = { async method() { await bar }}`,
884+
`await bar`,
885+
false
886+
)
855887
// class method
856888
assertAwaitDetection(
857889
`const cls = class Foo { async method() { await bar }}`,
890+
`await bar`,
858891
false
859892
)
860893
})

packages/compiler-sfc/src/compileScript.ts

+5
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,11 @@ export function compileScript(
900900
}
901901
if (node.type === 'AwaitExpression') {
902902
hasAwait = true
903+
s.prependRight(
904+
node.argument.start! + startOffset,
905+
helper(`withAsyncContext`) + `(`
906+
)
907+
s.appendLeft(node.argument.end! + startOffset, `)`)
903908
}
904909
}
905910
})

packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import {
2+
ComponentInternalInstance,
23
defineComponent,
4+
getCurrentInstance,
35
h,
46
nodeOps,
7+
onMounted,
58
render,
6-
SetupContext
9+
SetupContext,
10+
Suspense
711
} from '@vue/runtime-test'
812
import {
913
defineEmits,
@@ -12,7 +16,8 @@ import {
1216
withDefaults,
1317
useAttrs,
1418
useSlots,
15-
mergeDefaults
19+
mergeDefaults,
20+
withAsyncContext
1621
} from '../src/apiSetupHelpers'
1722

1823
describe('SFC <script setup> helpers', () => {
@@ -89,4 +94,39 @@ describe('SFC <script setup> helpers', () => {
8994
`props default key "foo" has no corresponding declaration`
9095
).toHaveBeenWarned()
9196
})
97+
98+
test('withAsyncContext', async () => {
99+
const spy = jest.fn()
100+
101+
let beforeInstance: ComponentInternalInstance | null = null
102+
let afterInstance: ComponentInternalInstance | null = null
103+
let resolve: (msg: string) => void
104+
105+
const Comp = defineComponent({
106+
async setup() {
107+
beforeInstance = getCurrentInstance()
108+
const msg = await withAsyncContext(
109+
new Promise(r => {
110+
resolve = r
111+
})
112+
)
113+
// register the lifecycle after an await statement
114+
onMounted(spy)
115+
afterInstance = getCurrentInstance()
116+
return () => msg
117+
}
118+
})
119+
120+
const root = nodeOps.createElement('div')
121+
render(h(() => h(Suspense, () => h(Comp))), root)
122+
123+
expect(spy).not.toHaveBeenCalled()
124+
resolve!('hello')
125+
// wait a macro task tick for all micro ticks to resolve
126+
await new Promise(r => setTimeout(r))
127+
// mount hook should have been called
128+
expect(spy).toHaveBeenCalled()
129+
// should retain same instance before/after the await call
130+
expect(beforeInstance).toBe(afterInstance)
131+
})
92132
})

packages/runtime-core/src/apiSetupHelpers.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
getCurrentInstance,
33
SetupContext,
4-
createSetupContext
4+
createSetupContext,
5+
setCurrentInstance
56
} from './component'
67
import { EmitFn, EmitsOptions } from './componentEmits'
78
import {
@@ -226,3 +227,17 @@ export function mergeDefaults(
226227
}
227228
return props
228229
}
230+
231+
/**
232+
* Runtime helper for storing and resuming current instance context in
233+
* async setup().
234+
* @internal
235+
*/
236+
export async function withAsyncContext<T>(
237+
awaitable: T | Promise<T>
238+
): Promise<T> {
239+
const ctx = getCurrentInstance()
240+
const res = await awaitable
241+
setCurrentInstance(ctx)
242+
return res
243+
}

packages/runtime-core/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ export { defineAsyncComponent } from './apiAsyncComponent'
4848
// <script setup> API ----------------------------------------------------------
4949

5050
export {
51+
// macros runtime, for warnings only
5152
defineProps,
5253
defineEmits,
5354
defineExpose,
5455
withDefaults,
5556
// internal
5657
mergeDefaults,
58+
withAsyncContext,
5759
// deprecated
5860
defineEmit,
5961
useContext

0 commit comments

Comments
 (0)