mirror of https://github.com/vuejs/core.git
feat(runtime-core): useId() (#11404)
This commit is contained in:
parent
3f8cbb2379
commit
73ef1561f6
|
|
@ -0,0 +1,242 @@
|
|||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import {
|
||||
type App,
|
||||
Suspense,
|
||||
createApp,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
h,
|
||||
useId,
|
||||
} from 'vue'
|
||||
import { renderToString } from '@vue/server-renderer'
|
||||
|
||||
type TestCaseFactory = () => [App, Promise<any>[]]
|
||||
|
||||
async function runOnClient(factory: TestCaseFactory) {
|
||||
const [app, deps] = factory()
|
||||
const root = document.createElement('div')
|
||||
app.mount(root)
|
||||
await Promise.all(deps)
|
||||
await promiseWithDelay(null, 0)
|
||||
return root.innerHTML
|
||||
}
|
||||
|
||||
async function runOnServer(factory: TestCaseFactory) {
|
||||
const [app, _] = factory()
|
||||
return (await renderToString(app))
|
||||
.replace(/<!--[\[\]]-->/g, '') // remove fragment wrappers
|
||||
.trim()
|
||||
}
|
||||
|
||||
async function getOutput(factory: TestCaseFactory) {
|
||||
const clientResult = await runOnClient(factory)
|
||||
const serverResult = await runOnServer(factory)
|
||||
expect(serverResult).toBe(clientResult)
|
||||
return clientResult
|
||||
}
|
||||
|
||||
function promiseWithDelay(res: any, delay: number) {
|
||||
return new Promise<any>(r => {
|
||||
setTimeout(() => r(res), delay)
|
||||
})
|
||||
}
|
||||
|
||||
const BasicComponentWithUseId = defineComponent({
|
||||
setup() {
|
||||
const id1 = useId()
|
||||
const id2 = useId()
|
||||
return () => [id1, ' ', id2]
|
||||
},
|
||||
})
|
||||
|
||||
describe('useId', () => {
|
||||
test('basic', async () => {
|
||||
expect(
|
||||
await getOutput(() => {
|
||||
const app = createApp(BasicComponentWithUseId)
|
||||
return [app, []]
|
||||
}),
|
||||
).toBe('v:0 v:1')
|
||||
})
|
||||
|
||||
test('with config.idPrefix', async () => {
|
||||
expect(
|
||||
await getOutput(() => {
|
||||
const app = createApp(BasicComponentWithUseId)
|
||||
app.config.idPrefix = 'foo'
|
||||
return [app, []]
|
||||
}),
|
||||
).toBe('foo:0 foo:1')
|
||||
})
|
||||
|
||||
test('async component', async () => {
|
||||
const factory = (
|
||||
delay1: number,
|
||||
delay2: number,
|
||||
): ReturnType<TestCaseFactory> => {
|
||||
const p1 = promiseWithDelay(BasicComponentWithUseId, delay1)
|
||||
const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
|
||||
const AsyncOne = defineAsyncComponent(() => p1)
|
||||
const AsyncTwo = defineAsyncComponent(() => p2)
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const id1 = useId()
|
||||
const id2 = useId()
|
||||
return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)]
|
||||
},
|
||||
})
|
||||
return [app, [p1, p2]]
|
||||
}
|
||||
|
||||
const expected =
|
||||
'v:0 v:1 ' + // root
|
||||
'v:0-0 v:0-1 ' + // inside first async subtree
|
||||
'v:1-0 v:1-1' // inside second async subtree
|
||||
// assert different async resolution order does not affect id stable-ness
|
||||
expect(await getOutput(() => factory(10, 20))).toBe(expected)
|
||||
expect(await getOutput(() => factory(20, 10))).toBe(expected)
|
||||
})
|
||||
|
||||
test('serverPrefetch', async () => {
|
||||
const factory = (
|
||||
delay1: number,
|
||||
delay2: number,
|
||||
): ReturnType<TestCaseFactory> => {
|
||||
const p1 = promiseWithDelay(null, delay1)
|
||||
const p2 = promiseWithDelay(null, delay2)
|
||||
|
||||
const SPOne = defineComponent({
|
||||
async serverPrefetch() {
|
||||
await p1
|
||||
},
|
||||
render() {
|
||||
return h(BasicComponentWithUseId)
|
||||
},
|
||||
})
|
||||
|
||||
const SPTwo = defineComponent({
|
||||
async serverPrefetch() {
|
||||
await p2
|
||||
},
|
||||
render() {
|
||||
return h(BasicComponentWithUseId)
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const id1 = useId()
|
||||
const id2 = useId()
|
||||
return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
|
||||
},
|
||||
})
|
||||
return [app, [p1, p2]]
|
||||
}
|
||||
|
||||
const expected =
|
||||
'v:0 v:1 ' + // root
|
||||
'v:0-0 v:0-1 ' + // inside first async subtree
|
||||
'v:1-0 v:1-1' // inside second async subtree
|
||||
// assert different async resolution order does not affect id stable-ness
|
||||
expect(await getOutput(() => factory(10, 20))).toBe(expected)
|
||||
expect(await getOutput(() => factory(20, 10))).toBe(expected)
|
||||
})
|
||||
|
||||
test('async setup()', async () => {
|
||||
const factory = (
|
||||
delay1: number,
|
||||
delay2: number,
|
||||
): ReturnType<TestCaseFactory> => {
|
||||
const p1 = promiseWithDelay(null, delay1)
|
||||
const p2 = promiseWithDelay(null, delay2)
|
||||
|
||||
const ASOne = defineComponent({
|
||||
async setup() {
|
||||
await p1
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return h(BasicComponentWithUseId)
|
||||
},
|
||||
})
|
||||
|
||||
const ASTwo = defineComponent({
|
||||
async setup() {
|
||||
await p2
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return h(BasicComponentWithUseId)
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const id1 = useId()
|
||||
const id2 = useId()
|
||||
return () =>
|
||||
h(Suspense, null, {
|
||||
default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]),
|
||||
})
|
||||
},
|
||||
})
|
||||
return [app, [p1, p2]]
|
||||
}
|
||||
|
||||
const expected =
|
||||
'<div>' +
|
||||
'v:0 v:1 ' + // root
|
||||
'v:0-0 v:0-1 ' + // inside first async subtree
|
||||
'v:1-0 v:1-1' + // inside second async subtree
|
||||
'</div>'
|
||||
// assert different async resolution order does not affect id stable-ness
|
||||
expect(await getOutput(() => factory(10, 20))).toBe(expected)
|
||||
expect(await getOutput(() => factory(20, 10))).toBe(expected)
|
||||
})
|
||||
|
||||
test('deep nested', async () => {
|
||||
const factory = (): ReturnType<TestCaseFactory> => {
|
||||
const p = Promise.resolve()
|
||||
const One = {
|
||||
async setup() {
|
||||
const id = useId()
|
||||
await p
|
||||
return () => [id, ' ', h(Two), ' ', h(Three)]
|
||||
},
|
||||
}
|
||||
const Two = {
|
||||
async setup() {
|
||||
const id = useId()
|
||||
await p
|
||||
return () => [id, ' ', h(Three), ' ', h(Three)]
|
||||
},
|
||||
}
|
||||
const Three = {
|
||||
async setup() {
|
||||
const id = useId()
|
||||
return () => id
|
||||
},
|
||||
}
|
||||
const app = createApp({
|
||||
setup() {
|
||||
return () =>
|
||||
h(Suspense, null, {
|
||||
default: h(One),
|
||||
})
|
||||
},
|
||||
})
|
||||
return [app, [p]]
|
||||
}
|
||||
|
||||
const expected =
|
||||
'v:0 ' + // One
|
||||
'v:0-0 ' + // Two
|
||||
'v:0-0-0 v:0-0-1 ' + // Three + Three nested in Two
|
||||
'v:0-1' // Three after Two
|
||||
// assert different async resolution order does not affect id stable-ness
|
||||
expect(await getOutput(() => factory())).toBe(expected)
|
||||
expect(await getOutput(() => factory())).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
|
@ -15,6 +15,7 @@ import { ref } from '@vue/reactivity'
|
|||
import { ErrorCodes, handleError } from './errorHandling'
|
||||
import { isKeepAlive } from './components/KeepAlive'
|
||||
import { queueJob } from './scheduler'
|
||||
import { markAsyncBoundary } from './helpers/useId'
|
||||
|
||||
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
|
||||
|
||||
|
|
@ -157,6 +158,8 @@ export function defineAsyncComponent<
|
|||
})
|
||||
: null
|
||||
})
|
||||
} else {
|
||||
markAsyncBoundary(instance)
|
||||
}
|
||||
|
||||
const loaded = ref(false)
|
||||
|
|
|
|||
|
|
@ -131,6 +131,11 @@ export interface AppConfig {
|
|||
* But in some cases, e.g. SSR, throwing might be more desirable.
|
||||
*/
|
||||
throwUnhandledErrorInProduction?: boolean
|
||||
|
||||
/**
|
||||
* Prefix for all useId() calls within this app
|
||||
*/
|
||||
idPrefix?: string
|
||||
}
|
||||
|
||||
export interface AppContext {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ import type { SuspenseProps } from './components/Suspense'
|
|||
import type { KeepAliveProps } from './components/KeepAlive'
|
||||
import type { BaseTransitionProps } from './components/BaseTransition'
|
||||
import type { DefineComponent } from './apiDefineComponent'
|
||||
import { markAsyncBoundary } from './helpers/useId'
|
||||
|
||||
export type Data = Record<string, unknown>
|
||||
|
||||
|
|
@ -356,6 +357,13 @@ export interface ComponentInternalInstance {
|
|||
* @internal
|
||||
*/
|
||||
provides: Data
|
||||
/**
|
||||
* for tracking useId()
|
||||
* first element is the current boundary prefix
|
||||
* second number is the index of the useId call within that boundary
|
||||
* @internal
|
||||
*/
|
||||
ids: [string, number, number]
|
||||
/**
|
||||
* Tracking reactive effects (e.g. watchers) associated with this component
|
||||
* so that they can be automatically stopped on component unmount
|
||||
|
|
@ -619,6 +627,7 @@ export function createComponentInstance(
|
|||
withProxy: null,
|
||||
|
||||
provides: parent ? parent.provides : Object.create(appContext.provides),
|
||||
ids: parent ? parent.ids : ['', 0, 0],
|
||||
accessCache: null!,
|
||||
renderCache: [],
|
||||
|
||||
|
|
@ -862,6 +871,8 @@ function setupStatefulComponent(
|
|||
reset()
|
||||
|
||||
if (isPromise(setupResult)) {
|
||||
// async setup, mark as async boundary for useId()
|
||||
markAsyncBoundary(instance)
|
||||
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
|
||||
if (isSSR) {
|
||||
// return the promise so server-renderer can wait on it
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ import {
|
|||
type ComponentTypeEmits,
|
||||
normalizePropsOrEmits,
|
||||
} from './apiSetupHelpers'
|
||||
import { markAsyncBoundary } from './helpers/useId'
|
||||
|
||||
/**
|
||||
* Interface for declaring custom options.
|
||||
|
|
@ -771,6 +772,10 @@ export function applyOptions(instance: ComponentInternalInstance) {
|
|||
) {
|
||||
instance.filters = filters
|
||||
}
|
||||
|
||||
if (__SSR__ && serverPrefetch) {
|
||||
markAsyncBoundary(instance)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveInjections(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
type ComponentInternalInstance,
|
||||
getCurrentInstance,
|
||||
} from '../component'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export function useId() {
|
||||
const i = getCurrentInstance()
|
||||
if (i) {
|
||||
return (i.appContext.config.idPrefix || 'v') + ':' + i.ids[0] + i.ids[1]++
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`useId() is called when there is no active component ` +
|
||||
`instance to be associated with.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There are 3 types of async boundaries:
|
||||
* - async components
|
||||
* - components with async setup()
|
||||
* - components with serverPrefetch
|
||||
*/
|
||||
export function markAsyncBoundary(instance: ComponentInternalInstance) {
|
||||
instance.ids = [instance.ids[0] + instance.ids[2]++ + '-', 0, 0]
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ export { defineAsyncComponent } from './apiAsyncComponent'
|
|||
export { useAttrs, useSlots } from './apiSetupHelpers'
|
||||
export { useModel } from './helpers/useModel'
|
||||
export { useTemplateRef } from './helpers/useTemplateRef'
|
||||
export { useId } from './helpers/useId'
|
||||
|
||||
// <script setup> API ----------------------------------------------------------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue