mirror of https://github.com/vuejs/core.git
feat(teleport): support deferred Teleport (#11387)
close #2015 close #11386
This commit is contained in:
parent
3ba70e49b5
commit
59a3e88903
|
@ -7,19 +7,98 @@ import {
|
||||||
Text,
|
Text,
|
||||||
createApp,
|
createApp,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
h,
|
|
||||||
markRaw,
|
markRaw,
|
||||||
nextTick,
|
nextTick,
|
||||||
nodeOps,
|
nodeOps,
|
||||||
|
h as originalH,
|
||||||
ref,
|
ref,
|
||||||
render,
|
render,
|
||||||
serializeInner,
|
serializeInner,
|
||||||
withDirectives,
|
withDirectives,
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
|
import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
|
||||||
import { compile, render as domRender } from 'vue'
|
import { compile, createApp as createDOMApp, render as domRender } from 'vue'
|
||||||
|
|
||||||
describe('renderer: teleport', () => {
|
describe('renderer: teleport', () => {
|
||||||
|
describe('eager mode', () => {
|
||||||
|
runSharedTests(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('defer mode', () => {
|
||||||
|
runSharedTests(true)
|
||||||
|
|
||||||
|
const h = originalH
|
||||||
|
|
||||||
|
test('should be able to target content appearing later than the teleport with defer', () => {
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
|
||||||
|
createDOMApp({
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
h(Teleport, { to: '#target', defer: true }, h('div', 'teleported')),
|
||||||
|
h('div', { id: 'target' }),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
expect(root.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<!--teleport start--><!--teleport end--><div id="target"><div>teleported</div></div>"`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('defer mode should work inside suspense', async () => {
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
|
||||||
|
let p: Promise<any>
|
||||||
|
|
||||||
|
const Comp = defineComponent({
|
||||||
|
template: `
|
||||||
|
<suspense>
|
||||||
|
<div>
|
||||||
|
<async />
|
||||||
|
<teleport defer to="#target-suspense">
|
||||||
|
<div ref="tel">teleported</div>
|
||||||
|
</teleport>
|
||||||
|
<div id="target-suspense" />
|
||||||
|
</div>
|
||||||
|
</suspense>`,
|
||||||
|
components: {
|
||||||
|
async: {
|
||||||
|
setup() {
|
||||||
|
p = Promise.resolve(() => 'async')
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
domRender(h(Comp), root)
|
||||||
|
expect(root.innerHTML).toBe(`<!---->`)
|
||||||
|
|
||||||
|
await p!.then(() => Promise.resolve())
|
||||||
|
await nextTick()
|
||||||
|
expect(root.innerHTML).toBe(
|
||||||
|
`<div>` +
|
||||||
|
`async` +
|
||||||
|
`<!--teleport start--><!--teleport end-->` +
|
||||||
|
`<div id="target-suspense"><div>teleported</div></div>` +
|
||||||
|
`</div>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function runSharedTests(deferMode: boolean) {
|
||||||
|
const h = (deferMode
|
||||||
|
? (type: any, props: any, ...args: any[]) => {
|
||||||
|
if (type === Teleport) {
|
||||||
|
props.defer = true
|
||||||
|
}
|
||||||
|
return originalH(type, props, ...args)
|
||||||
|
}
|
||||||
|
: originalH) as unknown as typeof originalH
|
||||||
|
|
||||||
test('should work', () => {
|
test('should work', () => {
|
||||||
const target = nodeOps.createElement('div')
|
const target = nodeOps.createElement('div')
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
|
@ -135,7 +214,10 @@ describe('renderer: teleport', () => {
|
||||||
|
|
||||||
function testUnmount(props: any) {
|
function testUnmount(props: any) {
|
||||||
render(
|
render(
|
||||||
h(() => [h(Teleport, props, h('div', 'teleported')), h('div', 'root')]),
|
h(() => [
|
||||||
|
h(Teleport, props, h('div', 'teleported')),
|
||||||
|
h('div', 'root'),
|
||||||
|
]),
|
||||||
root,
|
root,
|
||||||
)
|
)
|
||||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||||
|
@ -212,7 +294,9 @@ describe('renderer: teleport', () => {
|
||||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||||
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
|
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`,
|
||||||
)
|
)
|
||||||
expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>one</div>two"`)
|
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||||
|
`"<div>one</div>two"`,
|
||||||
|
)
|
||||||
|
|
||||||
// update existing content
|
// update existing content
|
||||||
render(
|
render(
|
||||||
|
@ -490,6 +574,7 @@ describe('renderer: teleport', () => {
|
||||||
`"<!--teleport start--><!--teleport end-->"`,
|
`"<!--teleport start--><!--teleport end-->"`,
|
||||||
)
|
)
|
||||||
expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>foo</div>"`)
|
expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>foo</div>"`)
|
||||||
|
await nextTick()
|
||||||
expect(dir.mounted).toHaveBeenCalledTimes(1)
|
expect(dir.mounted).toHaveBeenCalledTimes(1)
|
||||||
expect(dir.unmounted).toHaveBeenCalledTimes(0)
|
expect(dir.unmounted).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
|
@ -620,4 +705,5 @@ describe('renderer: teleport', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
|
expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -618,7 +618,7 @@ describe('renderer: optimized mode', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
//#3623
|
//#3623
|
||||||
test('nested teleport unmount need exit the optimization mode', () => {
|
test('nested teleport unmount need exit the optimization mode', async () => {
|
||||||
const target = nodeOps.createElement('div')
|
const target = nodeOps.createElement('div')
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
|
|
||||||
|
@ -647,6 +647,7 @@ describe('renderer: optimized mode', () => {
|
||||||
])),
|
])),
|
||||||
root,
|
root,
|
||||||
)
|
)
|
||||||
|
await nextTick()
|
||||||
expect(inner(target)).toMatchInlineSnapshot(
|
expect(inner(target)).toMatchInlineSnapshot(
|
||||||
`"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
|
`"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type RendererInternals,
|
type RendererInternals,
|
||||||
type RendererNode,
|
type RendererNode,
|
||||||
type RendererOptions,
|
type RendererOptions,
|
||||||
|
queuePostRenderEffect,
|
||||||
traverseStaticChildren,
|
traverseStaticChildren,
|
||||||
} from '../renderer'
|
} from '../renderer'
|
||||||
import type { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
|
import type { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
|
||||||
|
@ -19,6 +20,7 @@ export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
|
||||||
export interface TeleportProps {
|
export interface TeleportProps {
|
||||||
to: string | RendererElement | null | undefined
|
to: string | RendererElement | null | undefined
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
defer?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeleportEndKey = Symbol('_vte')
|
export const TeleportEndKey = Symbol('_vte')
|
||||||
|
@ -28,6 +30,9 @@ export const isTeleport = (type: any): boolean => type.__isTeleport
|
||||||
const isTeleportDisabled = (props: VNode['props']): boolean =>
|
const isTeleportDisabled = (props: VNode['props']): boolean =>
|
||||||
props && (props.disabled || props.disabled === '')
|
props && (props.disabled || props.disabled === '')
|
||||||
|
|
||||||
|
const isTeleportDeferred = (props: VNode['props']): boolean =>
|
||||||
|
props && (props.defer || props.defer === '')
|
||||||
|
|
||||||
const isTargetSVG = (target: RendererElement): boolean =>
|
const isTargetSVG = (target: RendererElement): boolean =>
|
||||||
typeof SVGElement !== 'undefined' && target instanceof SVGElement
|
typeof SVGElement !== 'undefined' && target instanceof SVGElement
|
||||||
|
|
||||||
|
@ -107,7 +112,6 @@ export const TeleportImpl = {
|
||||||
const mainAnchor = (n2.anchor = __DEV__
|
const mainAnchor = (n2.anchor = __DEV__
|
||||||
? createComment('teleport end')
|
? createComment('teleport end')
|
||||||
: createText(''))
|
: createText(''))
|
||||||
const target = (n2.target = resolveTarget(n2.props, querySelector))
|
|
||||||
const targetStart = (n2.targetStart = createText(''))
|
const targetStart = (n2.targetStart = createText(''))
|
||||||
const targetAnchor = (n2.targetAnchor = createText(''))
|
const targetAnchor = (n2.targetAnchor = createText(''))
|
||||||
insert(placeholder, container, anchor)
|
insert(placeholder, container, anchor)
|
||||||
|
@ -115,18 +119,6 @@ export const TeleportImpl = {
|
||||||
// attach a special property so we can skip teleported content in
|
// attach a special property so we can skip teleported content in
|
||||||
// renderer's nextSibling search
|
// renderer's nextSibling search
|
||||||
targetStart[TeleportEndKey] = targetAnchor
|
targetStart[TeleportEndKey] = targetAnchor
|
||||||
if (target) {
|
|
||||||
insert(targetStart, target)
|
|
||||||
insert(targetAnchor, target)
|
|
||||||
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
|
|
||||||
if (namespace === 'svg' || isTargetSVG(target)) {
|
|
||||||
namespace = 'svg'
|
|
||||||
} else if (namespace === 'mathml' || isTargetMathML(target)) {
|
|
||||||
namespace = 'mathml'
|
|
||||||
}
|
|
||||||
} else if (__DEV__ && !disabled) {
|
|
||||||
warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mount = (container: RendererElement, anchor: RendererNode) => {
|
const mount = (container: RendererElement, anchor: RendererNode) => {
|
||||||
// Teleport *always* has Array children. This is enforced in both the
|
// Teleport *always* has Array children. This is enforced in both the
|
||||||
|
@ -145,10 +137,39 @@ export const TeleportImpl = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mountToTarget = () => {
|
||||||
|
const target = (n2.target = resolveTarget(n2.props, querySelector))
|
||||||
|
if (target) {
|
||||||
|
insert(targetStart, target)
|
||||||
|
insert(targetAnchor, target)
|
||||||
|
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
|
||||||
|
if (namespace !== 'svg' && isTargetSVG(target)) {
|
||||||
|
namespace = 'svg'
|
||||||
|
} else if (namespace !== 'mathml' && isTargetMathML(target)) {
|
||||||
|
namespace = 'mathml'
|
||||||
|
}
|
||||||
|
if (!disabled) {
|
||||||
|
mount(target, targetAnchor)
|
||||||
|
updateCssVars(n2)
|
||||||
|
}
|
||||||
|
} else if (__DEV__ && !disabled) {
|
||||||
|
warn(
|
||||||
|
'Invalid Teleport target on mount:',
|
||||||
|
target,
|
||||||
|
`(${typeof target})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
mount(container, mainAnchor)
|
mount(container, mainAnchor)
|
||||||
} else if (target) {
|
updateCssVars(n2)
|
||||||
mount(target, targetAnchor)
|
}
|
||||||
|
|
||||||
|
if (isTeleportDeferred(n2.props)) {
|
||||||
|
queuePostRenderEffect(mountToTarget, parentSuspense)
|
||||||
|
} else {
|
||||||
|
mountToTarget()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// update content
|
// update content
|
||||||
|
@ -249,9 +270,8 @@ export const TeleportImpl = {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
updateCssVars(n2)
|
updateCssVars(n2)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(
|
remove(
|
||||||
|
@ -441,7 +461,7 @@ function updateCssVars(vnode: VNode) {
|
||||||
// code path here can assume browser environment.
|
// code path here can assume browser environment.
|
||||||
const ctx = vnode.ctx
|
const ctx = vnode.ctx
|
||||||
if (ctx && ctx.ut) {
|
if (ctx && ctx.ut) {
|
||||||
let node = (vnode.children as VNode[])[0].el!
|
let node = vnode.targetStart
|
||||||
while (node && node !== vnode.targetAnchor) {
|
while (node && node !== vnode.targetAnchor) {
|
||||||
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
|
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
|
||||||
node = node.nextSibling
|
node = node.nextSibling
|
||||||
|
|
|
@ -127,7 +127,9 @@ export function invalidateJob(job: SchedulerJob) {
|
||||||
|
|
||||||
export function queuePostFlushCb(cb: SchedulerJobs) {
|
export function queuePostFlushCb(cb: SchedulerJobs) {
|
||||||
if (!isArray(cb)) {
|
if (!isArray(cb)) {
|
||||||
if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
|
if (activePostFlushCbs && cb.id === -1) {
|
||||||
|
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
|
||||||
|
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
|
||||||
pendingPostFlushCbs.push(cb)
|
pendingPostFlushCbs.push(cb)
|
||||||
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
|
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
|
||||||
cb.flags! |= SchedulerJobFlags.QUEUED
|
cb.flags! |= SchedulerJobFlags.QUEUED
|
||||||
|
|
Loading…
Reference in New Issue