feat(sfc): auto restore current instance after await statements in async setup()
This commit is contained in:
parent
fd7fa6f694
commit
0240e82a38
@ -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
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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, `)`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user