fix(sfc): fix <script setup>
async context preservation logic
fix #4050
This commit is contained in:
parent
b68dfbb9da
commit
03e26845e2
@ -847,6 +847,9 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
const { content } = compile(`<script setup>${code}</script>`, {
|
const { content } = compile(`<script setup>${code}</script>`, {
|
||||||
refSugar: true
|
refSugar: true
|
||||||
})
|
})
|
||||||
|
if (shouldAsync) {
|
||||||
|
expect(content).toMatch(`let __temp, __restore`)
|
||||||
|
}
|
||||||
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
|
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
|
||||||
if (typeof expected === 'string') {
|
if (typeof expected === 'string') {
|
||||||
expect(content).toMatch(expected)
|
expect(content).toMatch(expected)
|
||||||
@ -856,28 +859,35 @@ const emit = defineEmits(['a', 'b'])
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('expression statement', () => {
|
test('expression statement', () => {
|
||||||
assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
|
assertAwaitDetection(
|
||||||
|
`await foo`,
|
||||||
|
`;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('variable', () => {
|
test('variable', () => {
|
||||||
assertAwaitDetection(
|
assertAwaitDetection(
|
||||||
`const a = 1 + (await foo)`,
|
`const a = 1 + (await foo)`,
|
||||||
`1 + (await _withAsyncContext(foo))`
|
`1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('ref', () => {
|
test('ref', () => {
|
||||||
assertAwaitDetection(
|
assertAwaitDetection(
|
||||||
`ref: a = 1 + (await foo)`,
|
`ref: a = 1 + (await foo)`,
|
||||||
`1 + (await _withAsyncContext(foo))`
|
`1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested statements', () => {
|
test('nested statements', () => {
|
||||||
assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
|
assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
|
||||||
return (
|
return (
|
||||||
code.includes(`await _withAsyncContext(foo)`) &&
|
code.includes(
|
||||||
code.includes(`await _withAsyncContext(bar)`)
|
`;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
|
||||||
|
) &&
|
||||||
|
code.includes(
|
||||||
|
`;(([__temp,__restore]=_withAsyncContext(()=>(bar))),__temp=await __temp,__restore())`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,8 @@ import {
|
|||||||
LabeledStatement,
|
LabeledStatement,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
RestElement,
|
RestElement,
|
||||||
TSInterfaceBody
|
TSInterfaceBody,
|
||||||
|
AwaitExpression
|
||||||
} from '@babel/types'
|
} from '@babel/types'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import { RawSourceMap } from 'source-map'
|
import { RawSourceMap } from 'source-map'
|
||||||
@ -487,6 +488,25 @@ export function compileScript(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* await foo()
|
||||||
|
* -->
|
||||||
|
* (([__temp, __restore] = withAsyncContext(() => foo())),__temp=await __temp,__restore(),__temp)
|
||||||
|
*/
|
||||||
|
function processAwait(node: AwaitExpression, isStatement: boolean) {
|
||||||
|
s.overwrite(
|
||||||
|
node.start! + startOffset,
|
||||||
|
node.argument.start! + startOffset,
|
||||||
|
`${isStatement ? `;` : ``}(([__temp,__restore]=${helper(
|
||||||
|
`withAsyncContext`
|
||||||
|
)}(()=>(`
|
||||||
|
)
|
||||||
|
s.appendLeft(
|
||||||
|
node.end! + startOffset,
|
||||||
|
`))),__temp=await __temp,__restore()${isStatement ? `` : `,__temp`})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function processRefExpression(exp: Expression, statement: LabeledStatement) {
|
function processRefExpression(exp: Expression, statement: LabeledStatement) {
|
||||||
if (exp.type === 'AssignmentExpression') {
|
if (exp.type === 'AssignmentExpression') {
|
||||||
const { left, right } = exp
|
const { left, right } = exp
|
||||||
@ -949,17 +969,13 @@ export function compileScript(
|
|||||||
node.type.endsWith('Statement')
|
node.type.endsWith('Statement')
|
||||||
) {
|
) {
|
||||||
;(walk as any)(node, {
|
;(walk as any)(node, {
|
||||||
enter(node: Node) {
|
enter(child: Node, parent: Node) {
|
||||||
if (isFunction(node)) {
|
if (isFunction(child)) {
|
||||||
this.skip()
|
this.skip()
|
||||||
}
|
}
|
||||||
if (node.type === 'AwaitExpression') {
|
if (child.type === 'AwaitExpression') {
|
||||||
hasAwait = true
|
hasAwait = true
|
||||||
s.prependRight(
|
processAwait(child, parent.type === 'ExpressionStatement')
|
||||||
node.argument.start! + startOffset,
|
|
||||||
helper(`withAsyncContext`) + `(`
|
|
||||||
)
|
|
||||||
s.appendLeft(node.argument.end! + startOffset, `)`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1151,6 +1167,11 @@ export function compileScript(
|
|||||||
if (propsIdentifier) {
|
if (propsIdentifier) {
|
||||||
s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
|
s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
|
||||||
}
|
}
|
||||||
|
// inject temp variables for async context preservation
|
||||||
|
if (hasAwait) {
|
||||||
|
const any = isTS ? `:any` : ``
|
||||||
|
s.prependRight(startOffset, `\nlet __temp${any}, __restore${any}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
const destructureElements =
|
const destructureElements =
|
||||||
hasDefineExposeCall || !options.inlineTemplate ? [`expose`] : []
|
hasDefineExposeCall || !options.inlineTemplate ? [`expose`] : []
|
||||||
|
@ -119,12 +119,20 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
async setup() {
|
async setup() {
|
||||||
|
let __temp: any, __restore: any
|
||||||
|
|
||||||
beforeInstance = getCurrentInstance()
|
beforeInstance = getCurrentInstance()
|
||||||
const msg = await withAsyncContext(
|
|
||||||
|
const msg = (([__temp, __restore] = withAsyncContext(
|
||||||
|
() =>
|
||||||
new Promise(r => {
|
new Promise(r => {
|
||||||
resolve = r
|
resolve = r
|
||||||
})
|
})
|
||||||
)
|
)),
|
||||||
|
(__temp = await __temp),
|
||||||
|
__restore(),
|
||||||
|
__temp)
|
||||||
|
|
||||||
// register the lifecycle after an await statement
|
// register the lifecycle after an await statement
|
||||||
onMounted(spy)
|
onMounted(spy)
|
||||||
afterInstance = getCurrentInstance()
|
afterInstance = getCurrentInstance()
|
||||||
@ -155,13 +163,18 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
async setup() {
|
async setup() {
|
||||||
|
let __temp: any, __restore: any
|
||||||
|
|
||||||
beforeInstance = getCurrentInstance()
|
beforeInstance = getCurrentInstance()
|
||||||
try {
|
try {
|
||||||
await withAsyncContext(
|
;[__temp, __restore] = withAsyncContext(
|
||||||
new Promise((r, rj) => {
|
() =>
|
||||||
|
new Promise((_, rj) => {
|
||||||
reject = rj
|
reject = rj
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -206,11 +219,20 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
async setup() {
|
async setup() {
|
||||||
|
let __temp: any, __restore: any
|
||||||
|
|
||||||
beforeInstance = getCurrentInstance()
|
beforeInstance = getCurrentInstance()
|
||||||
|
|
||||||
// first await
|
// first await
|
||||||
await withAsyncContext(Promise.resolve())
|
;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
|
|
||||||
// setup exit, instance set to null, then resumed
|
// setup exit, instance set to null, then resumed
|
||||||
await withAsyncContext(doAsyncWork())
|
;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
|
|
||||||
afterInstance = getCurrentInstance()
|
afterInstance = getCurrentInstance()
|
||||||
return () => {
|
return () => {
|
||||||
resolve()
|
resolve()
|
||||||
@ -237,8 +259,13 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
async setup() {
|
async setup() {
|
||||||
await withAsyncContext(Promise.resolve())
|
let __temp: any, __restore: any
|
||||||
await withAsyncContext(Promise.reject())
|
;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
|
;[__temp, __restore] = withAsyncContext(() => Promise.reject())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
},
|
},
|
||||||
render() {}
|
render() {}
|
||||||
})
|
})
|
||||||
@ -256,6 +283,42 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
expect(getCurrentInstance()).toBeNull()
|
expect(getCurrentInstance()).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// #4050
|
||||||
|
test('race conditions', async () => {
|
||||||
|
const uids = {
|
||||||
|
one: { before: NaN, after: NaN },
|
||||||
|
two: { before: NaN, after: NaN }
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = defineComponent({
|
||||||
|
props: ['name'],
|
||||||
|
async setup(props: { name: 'one' | 'two' }) {
|
||||||
|
let __temp: any, __restore: any
|
||||||
|
|
||||||
|
uids[props.name].before = getCurrentInstance()!.uid
|
||||||
|
;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
|
|
||||||
|
uids[props.name].after = getCurrentInstance()!.uid
|
||||||
|
return () => ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(() =>
|
||||||
|
h(Suspense, () =>
|
||||||
|
h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
app.mount(root)
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r))
|
||||||
|
expect(uids.one.before).not.toBe(uids.two.before)
|
||||||
|
expect(uids.one.before).toBe(uids.one.after)
|
||||||
|
expect(uids.two.before).toBe(uids.two.after)
|
||||||
|
})
|
||||||
|
|
||||||
test('should teardown in-scope effects', async () => {
|
test('should teardown in-scope effects', async () => {
|
||||||
let resolve: (val?: any) => void
|
let resolve: (val?: any) => void
|
||||||
const ready = new Promise(r => {
|
const ready = new Promise(r => {
|
||||||
@ -266,7 +329,10 @@ describe('SFC <script setup> helpers', () => {
|
|||||||
|
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
async setup() {
|
async setup() {
|
||||||
await withAsyncContext(Promise.resolve())
|
let __temp: any, __restore: any
|
||||||
|
;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
|
||||||
|
__temp = await __temp
|
||||||
|
__restore()
|
||||||
|
|
||||||
c = computed(() => {})
|
c = computed(() => {})
|
||||||
// register the lifecycle after an await statement
|
// register the lifecycle after an await statement
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { isPromise } from '../../shared/src'
|
import { isPromise } from '../../shared/src'
|
||||||
import {
|
import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
setCurrentInstance,
|
||||||
SetupContext,
|
SetupContext,
|
||||||
createSetupContext,
|
createSetupContext
|
||||||
setCurrentInstance
|
|
||||||
} from './component'
|
} from './component'
|
||||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||||
import {
|
import {
|
||||||
@ -230,25 +230,32 @@ export function mergeDefaults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime helper for storing and resuming current instance context in
|
* `<script setup>` helper for persisting the current instance context over
|
||||||
* async setup().
|
* async/await flows.
|
||||||
|
*
|
||||||
|
* `@vue/compiler-sfc` converts the following:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const x = await foo()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* into:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* let __temp, __restore
|
||||||
|
* const x = (([__temp, __restore] = withAsyncContext(() => foo())),__temp=await __temp,__restore(),__temp)
|
||||||
|
* ```
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function withAsyncContext<T>(awaitable: T | Promise<T>): Promise<T> {
|
export function withAsyncContext(getAwaitable: () => any) {
|
||||||
const ctx = getCurrentInstance()
|
const ctx = getCurrentInstance()
|
||||||
setCurrentInstance(null) // unset after storing instance
|
let awaitable = getAwaitable()
|
||||||
if (__DEV__ && !ctx) {
|
setCurrentInstance(null)
|
||||||
warn(`withAsyncContext() called when there is no active context instance.`)
|
if (isPromise(awaitable)) {
|
||||||
}
|
awaitable = awaitable.catch(e => {
|
||||||
return isPromise<T>(awaitable)
|
|
||||||
? awaitable.then(
|
|
||||||
res => {
|
|
||||||
setCurrentInstance(ctx)
|
setCurrentInstance(ctx)
|
||||||
return res
|
throw e
|
||||||
},
|
})
|
||||||
err => {
|
|
||||||
setCurrentInstance(ctx)
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
)
|
return [awaitable, () => setCurrentInstance(ctx)]
|
||||||
: (awaitable as any)
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user