feat(sfc): auto restore current instance after await statements in async setup()

This commit is contained in:
Evan You 2021-06-29 09:24:12 -04:00
parent fd7fa6f694
commit 0240e82a38
5 changed files with 106 additions and 11 deletions

View File

@ -824,37 +824,70 @@ const emit = defineEmits(['a', 'b'])
}) })
describe('async/await detection', () => { describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) { function assertAwaitDetection(
code: string,
expected: string | ((content: string) => boolean),
shouldAsync = true
) {
const { content } = compile(`<script setup>${code}</script>`) const { content } = compile(`<script setup>${code}</script>`)
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`) expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
if (typeof expected === 'string') {
expect(content).toMatch(expected)
} else {
expect(expected(content)).toBe(true)
}
} }
test('expression statement', () => { test('expression statement', () => {
assertAwaitDetection(`await foo`) assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
}) })
test('variable', () => { test('variable', () => {
assertAwaitDetection(`const a = 1 + (await foo)`) assertAwaitDetection(
`const a = 1 + (await foo)`,
`1 + (await _withAsyncContext(foo))`
)
}) })
test('ref', () => { test('ref', () => {
assertAwaitDetection(`ref: a = 1 + (await foo)`) assertAwaitDetection(
`ref: a = 1 + (await foo)`,
`1 + (await _withAsyncContext(foo))`
)
}) })
test('nested statements', () => { test('nested statements', () => {
assertAwaitDetection(`if (ok) { await foo } else { await bar }`) assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
return (
code.includes(`await _withAsyncContext(foo)`) &&
code.includes(`await _withAsyncContext(bar)`)
)
})
}) })
test('should ignore await inside functions', () => { test('should ignore await inside functions', () => {
// function declaration // function declaration
assertAwaitDetection(`async function foo() { await bar }`, false) assertAwaitDetection(
`async function foo() { await bar }`,
`await bar`,
false
)
// function expression // function expression
assertAwaitDetection(`const foo = async () => { await bar }`, false) assertAwaitDetection(
`const foo = async () => { await bar }`,
`await bar`,
false
)
// object method // object method
assertAwaitDetection(`const obj = { async method() { await bar }}`, false) assertAwaitDetection(
`const obj = { async method() { await bar }}`,
`await bar`,
false
)
// class method // class method
assertAwaitDetection( assertAwaitDetection(
`const cls = class Foo { async method() { await bar }}`, `const cls = class Foo { async method() { await bar }}`,
`await bar`,
false false
) )
}) })

View File

@ -900,6 +900,11 @@ export function compileScript(
} }
if (node.type === 'AwaitExpression') { if (node.type === 'AwaitExpression') {
hasAwait = true hasAwait = true
s.prependRight(
node.argument.start! + startOffset,
helper(`withAsyncContext`) + `(`
)
s.appendLeft(node.argument.end! + startOffset, `)`)
} }
} }
}) })

View File

@ -1,9 +1,13 @@
import { import {
ComponentInternalInstance,
defineComponent, defineComponent,
getCurrentInstance,
h, h,
nodeOps, nodeOps,
onMounted,
render, render,
SetupContext SetupContext,
Suspense
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { import {
defineEmits, defineEmits,
@ -12,7 +16,8 @@ import {
withDefaults, withDefaults,
useAttrs, useAttrs,
useSlots, useSlots,
mergeDefaults mergeDefaults,
withAsyncContext
} from '../src/apiSetupHelpers' } from '../src/apiSetupHelpers'
describe('SFC <script setup> helpers', () => { describe('SFC <script setup> helpers', () => {
@ -89,4 +94,39 @@ describe('SFC <script setup> helpers', () => {
`props default key "foo" has no corresponding declaration` `props default key "foo" has no corresponding declaration`
).toHaveBeenWarned() ).toHaveBeenWarned()
}) })
test('withAsyncContext', async () => {
const spy = jest.fn()
let beforeInstance: ComponentInternalInstance | null = null
let afterInstance: ComponentInternalInstance | null = null
let resolve: (msg: string) => void
const Comp = defineComponent({
async setup() {
beforeInstance = getCurrentInstance()
const msg = await withAsyncContext(
new Promise(r => {
resolve = r
})
)
// register the lifecycle after an await statement
onMounted(spy)
afterInstance = getCurrentInstance()
return () => msg
}
})
const root = nodeOps.createElement('div')
render(h(() => h(Suspense, () => h(Comp))), root)
expect(spy).not.toHaveBeenCalled()
resolve!('hello')
// wait a macro task tick for all micro ticks to resolve
await new Promise(r => setTimeout(r))
// mount hook should have been called
expect(spy).toHaveBeenCalled()
// should retain same instance before/after the await call
expect(beforeInstance).toBe(afterInstance)
})
}) })

View File

@ -1,7 +1,8 @@
import { import {
getCurrentInstance, getCurrentInstance,
SetupContext, SetupContext,
createSetupContext createSetupContext,
setCurrentInstance
} from './component' } from './component'
import { EmitFn, EmitsOptions } from './componentEmits' import { EmitFn, EmitsOptions } from './componentEmits'
import { import {
@ -226,3 +227,17 @@ export function mergeDefaults(
} }
return props return props
} }
/**
* Runtime helper for storing and resuming current instance context in
* async setup().
* @internal
*/
export async function withAsyncContext<T>(
awaitable: T | Promise<T>
): Promise<T> {
const ctx = getCurrentInstance()
const res = await awaitable
setCurrentInstance(ctx)
return res
}

View File

@ -48,12 +48,14 @@ export { defineAsyncComponent } from './apiAsyncComponent'
// <script setup> API ---------------------------------------------------------- // <script setup> API ----------------------------------------------------------
export { export {
// macros runtime, for warnings only
defineProps, defineProps,
defineEmits, defineEmits,
defineExpose, defineExpose,
withDefaults, withDefaults,
// internal // internal
mergeDefaults, mergeDefaults,
withAsyncContext,
// deprecated // deprecated
defineEmit, defineEmit,
useContext useContext