wip: somewhat working suspense
This commit is contained in:
parent
1dc9d81e3e
commit
02bb156314
@ -78,6 +78,10 @@ export interface ComponentInternalInstance {
|
|||||||
components: Record<string, Component>
|
components: Record<string, Component>
|
||||||
directives: Record<string, Directive>
|
directives: Record<string, Directive>
|
||||||
|
|
||||||
|
asyncDep: Promise<any> | null
|
||||||
|
asyncResult: any
|
||||||
|
asyncResolved: boolean
|
||||||
|
|
||||||
// the rest are only for stateful components
|
// the rest are only for stateful components
|
||||||
renderContext: Data
|
renderContext: Data
|
||||||
data: Data
|
data: Data
|
||||||
@ -146,6 +150,11 @@ export function createComponentInstance(
|
|||||||
components: Object.create(appContext.components),
|
components: Object.create(appContext.components),
|
||||||
directives: Object.create(appContext.directives),
|
directives: Object.create(appContext.directives),
|
||||||
|
|
||||||
|
// async dependency management
|
||||||
|
asyncDep: null,
|
||||||
|
asyncResult: null,
|
||||||
|
asyncResolved: false,
|
||||||
|
|
||||||
// user namespace for storing whatever the user assigns to `this`
|
// user namespace for storing whatever the user assigns to `this`
|
||||||
user: {},
|
user: {},
|
||||||
|
|
||||||
@ -206,7 +215,6 @@ export const setCurrentInstance = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
||||||
currentInstance = instance
|
|
||||||
const Component = instance.type as ComponentOptions
|
const Component = instance.type as ComponentOptions
|
||||||
// 1. create render proxy
|
// 1. create render proxy
|
||||||
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
|
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
|
||||||
@ -219,62 +227,76 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
|||||||
if (setup) {
|
if (setup) {
|
||||||
const setupContext = (instance.setupContext =
|
const setupContext = (instance.setupContext =
|
||||||
setup.length > 1 ? createSetupContext(instance) : null)
|
setup.length > 1 ? createSetupContext(instance) : null)
|
||||||
|
|
||||||
|
currentInstance = instance
|
||||||
const setupResult = callWithErrorHandling(
|
const setupResult = callWithErrorHandling(
|
||||||
setup,
|
setup,
|
||||||
instance,
|
instance,
|
||||||
ErrorCodes.SETUP_FUNCTION,
|
ErrorCodes.SETUP_FUNCTION,
|
||||||
[propsProxy, setupContext]
|
[propsProxy, setupContext]
|
||||||
)
|
)
|
||||||
|
currentInstance = null
|
||||||
|
|
||||||
if (isFunction(setupResult)) {
|
if (
|
||||||
// setup returned an inline render function
|
setupResult &&
|
||||||
instance.render = setupResult
|
isFunction(setupResult.then) &&
|
||||||
|
isFunction(setupResult.catch)
|
||||||
|
) {
|
||||||
|
// async setup returned Promise.
|
||||||
|
// bail here and wait for re-entry.
|
||||||
|
instance.asyncDep = setupResult as Promise<any>
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
if (__DEV__) {
|
handleSetupResult(instance, setupResult)
|
||||||
if (!Component.render) {
|
|
||||||
warn(
|
|
||||||
`Component is missing render function. Either provide a template or ` +
|
|
||||||
`return a render function from setup().`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
setupResult &&
|
|
||||||
typeof setupResult.then === 'function' &&
|
|
||||||
typeof setupResult.catch === 'function'
|
|
||||||
) {
|
|
||||||
warn(`setup() returned a Promise. setup() cannot be async.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// setup returned bindings.
|
|
||||||
// assuming a render function compiled from template is present.
|
|
||||||
if (isObject(setupResult)) {
|
|
||||||
instance.renderContext = reactive(setupResult)
|
|
||||||
} else if (__DEV__ && setupResult !== undefined) {
|
|
||||||
warn(
|
|
||||||
`setup() should return an object. Received: ${
|
|
||||||
setupResult === null ? 'null' : typeof setupResult
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
instance.render = (Component.render || NOOP) as RenderFunction
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
finishComponentSetup(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSetupResult(
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
setupResult: unknown
|
||||||
|
) {
|
||||||
|
if (isFunction(setupResult)) {
|
||||||
|
// setup returned an inline render function
|
||||||
|
instance.render = setupResult as RenderFunction
|
||||||
|
} else if (isObject(setupResult)) {
|
||||||
|
// setup returned bindings.
|
||||||
|
// assuming a render function compiled from template is present.
|
||||||
|
instance.renderContext = reactive(setupResult)
|
||||||
|
} else if (__DEV__ && setupResult !== undefined) {
|
||||||
|
warn(
|
||||||
|
`setup() should return an object. Received: ${
|
||||||
|
setupResult === null ? 'null' : typeof setupResult
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finishComponentSetup(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishComponentSetup(instance: ComponentInternalInstance) {
|
||||||
|
const Component = instance.type as ComponentOptions
|
||||||
|
if (!instance.render) {
|
||||||
if (__DEV__ && !Component.render) {
|
if (__DEV__ && !Component.render) {
|
||||||
warn(
|
warn(
|
||||||
`Component is missing render function. Either provide a template or ` +
|
`Component is missing render function. Either provide a template or ` +
|
||||||
`return a render function from setup().`
|
`return a render function from setup().`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
instance.render = Component.render as RenderFunction
|
instance.render = (Component.render || NOOP) as RenderFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
// support for 2.x options
|
// support for 2.x options
|
||||||
if (__FEATURE_OPTIONS__) {
|
if (__FEATURE_OPTIONS__) {
|
||||||
|
currentInstance = instance
|
||||||
applyOptions(instance, Component)
|
applyOptions(instance, Component)
|
||||||
|
currentInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.renderContext === EMPTY_OBJ) {
|
if (instance.renderContext === EMPTY_OBJ) {
|
||||||
instance.renderContext = reactive({})
|
instance.renderContext = reactive({})
|
||||||
}
|
}
|
||||||
currentInstance = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to identify a setup context proxy
|
// used to identify a setup context proxy
|
||||||
|
@ -12,7 +12,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
setupStatefulComponent
|
setupStatefulComponent,
|
||||||
|
handleSetupResult,
|
||||||
|
setCurrentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import {
|
||||||
renderComponentRoot,
|
renderComponentRoot,
|
||||||
@ -43,6 +45,7 @@ import { invokeDirectiveHook } from './directives'
|
|||||||
import { ComponentPublicInstance } from './componentPublicInstanceProxy'
|
import { ComponentPublicInstance } from './componentPublicInstanceProxy'
|
||||||
import { App, createAppAPI } from './apiApp'
|
import { App, createAppAPI } from './apiApp'
|
||||||
import { SuspenseBoundary, createSuspenseBoundary } from './suspense'
|
import { SuspenseBoundary, createSuspenseBoundary } from './suspense'
|
||||||
|
import { provide } from './apiInject'
|
||||||
|
|
||||||
const prodEffectOptions = {
|
const prodEffectOptions = {
|
||||||
scheduler: queueJob
|
scheduler: queueJob
|
||||||
@ -604,16 +607,68 @@ export function createRenderer<
|
|||||||
if (n1 == null) {
|
if (n1 == null) {
|
||||||
const contentContainer = hostCreateElement('div')
|
const contentContainer = hostCreateElement('div')
|
||||||
const suspense = (n2.suspense = createSuspenseBoundary(
|
const suspense = (n2.suspense = createSuspenseBoundary(
|
||||||
parentSuspense,
|
n2,
|
||||||
contentContainer
|
parentSuspense
|
||||||
))
|
))
|
||||||
|
|
||||||
|
suspense.onRetry(() => {
|
||||||
|
processFragment(
|
||||||
|
suspense.oldContentTree,
|
||||||
|
suspense.contentTree as HostVNode,
|
||||||
|
contentContainer,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
isSVG,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
|
if (suspense.deps > 0) {
|
||||||
|
// still pending.
|
||||||
|
// patch the fallback tree.
|
||||||
|
} else {
|
||||||
|
suspense.resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
suspense.onResolve(() => {
|
||||||
|
// move content from off-dom container to actual container
|
||||||
|
;(suspense.contentTree as any).children.forEach((vnode: HostVNode) => {
|
||||||
|
move(vnode, container, anchor)
|
||||||
|
})
|
||||||
|
suspense.vnode.el = (suspense.contentTree as HostVNode).el
|
||||||
|
// check if there is a pending parent suspense
|
||||||
|
let parent = suspense.parent
|
||||||
|
let hasUnresolvedAncestor = false
|
||||||
|
while (parent) {
|
||||||
|
if (!parent.isResolved) {
|
||||||
|
// found a pending parent suspense, merge buffered post jobs
|
||||||
|
// into that parent
|
||||||
|
parent.bufferedJobs.push(...suspense.bufferedJobs)
|
||||||
|
hasUnresolvedAncestor = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no pending parent suspense, flush all jobs
|
||||||
|
if (!hasUnresolvedAncestor) {
|
||||||
|
queuePostFlushCb(suspense.bufferedJobs)
|
||||||
|
}
|
||||||
|
suspense.isResolved = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO pass it down as an arg instead
|
||||||
|
if (parentComponent) {
|
||||||
|
setCurrentInstance(parentComponent)
|
||||||
|
provide('suspense', suspense)
|
||||||
|
setCurrentInstance(null)
|
||||||
|
}
|
||||||
|
|
||||||
// start mounting the subtree off-dom
|
// start mounting the subtree off-dom
|
||||||
// - TODO tracking async deps and buffering postQueue jobs on current boundary
|
// TODO should buffer postQueue jobs on current boundary
|
||||||
const contentTree = (suspense.contentTree = childrenToFragment(n2))
|
const contentTree = (suspense.contentTree = suspense.oldContentTree = childrenToFragment(
|
||||||
|
n2
|
||||||
|
))
|
||||||
processFragment(
|
processFragment(
|
||||||
null,
|
null,
|
||||||
contentTree as VNode<HostNode, HostElement>,
|
contentTree as HostVNode,
|
||||||
contentContainer,
|
contentContainer,
|
||||||
null,
|
null,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
@ -625,6 +680,7 @@ export function createRenderer<
|
|||||||
// yes: mount the fallback tree.
|
// yes: mount the fallback tree.
|
||||||
// Each time an async dep resolves, it pings the boundary
|
// Each time an async dep resolves, it pings the boundary
|
||||||
// and causes a re-entry.
|
// and causes a re-entry.
|
||||||
|
console.log('fallback')
|
||||||
} else {
|
} else {
|
||||||
suspense.resolve()
|
suspense.resolve()
|
||||||
}
|
}
|
||||||
@ -633,23 +689,23 @@ export function createRenderer<
|
|||||||
HostNode,
|
HostNode,
|
||||||
HostElement
|
HostElement
|
||||||
>
|
>
|
||||||
const oldContentTree = suspense.contentTree
|
suspense.vnode = n2
|
||||||
|
const oldContentTree = (suspense.oldContentTree = suspense.contentTree)
|
||||||
const newContentTree = (suspense.contentTree = childrenToFragment(n2))
|
const newContentTree = (suspense.contentTree = childrenToFragment(n2))
|
||||||
// patch suspense subTree as fragment
|
if (!suspense.isResolved) {
|
||||||
processFragment(
|
suspense.retry()
|
||||||
oldContentTree,
|
|
||||||
newContentTree,
|
|
||||||
container,
|
|
||||||
anchor,
|
|
||||||
parentComponent,
|
|
||||||
isSVG,
|
|
||||||
optimized
|
|
||||||
)
|
|
||||||
if (suspense.deps > 0) {
|
|
||||||
// still pending.
|
|
||||||
// patch the fallback tree.
|
|
||||||
} else {
|
} else {
|
||||||
suspense.resolve()
|
// just normal patch inner content as a fragment
|
||||||
|
processFragment(
|
||||||
|
oldContentTree,
|
||||||
|
newContentTree,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
isSVG,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
|
n2.el = newContentTree.el
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -676,10 +732,24 @@ export function createRenderer<
|
|||||||
} else {
|
} else {
|
||||||
const instance = (n2.component =
|
const instance = (n2.component =
|
||||||
n1.component) as ComponentInternalInstance
|
n1.component) as ComponentInternalInstance
|
||||||
if (shouldUpdateComponent(n1, n2, optimized)) {
|
// async still pending
|
||||||
|
if (instance.asyncDep && !instance.asyncResolved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// a resolved async component, on successful re-entry.
|
||||||
|
// pickup the mounting process and setup render effect
|
||||||
|
if (!instance.update) {
|
||||||
|
setupRenderEffect(instance, n2, container, anchor, isSVG)
|
||||||
|
} else if (
|
||||||
|
shouldUpdateComponent(n1, n2, optimized) ||
|
||||||
|
(instance.provides.suspense &&
|
||||||
|
!(instance.provides.suspense as any).isResolved)
|
||||||
|
) {
|
||||||
|
// normal update
|
||||||
instance.next = n2
|
instance.next = n2
|
||||||
instance.update()
|
instance.update()
|
||||||
} else {
|
} else {
|
||||||
|
// no update needed. just copy over properties
|
||||||
n2.component = n1.component
|
n2.component = n1.component
|
||||||
n2.el = n1.el
|
n2.el = n1.el
|
||||||
}
|
}
|
||||||
@ -720,6 +790,37 @@ export function createRenderer<
|
|||||||
setupStatefulComponent(instance)
|
setupStatefulComponent(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setup() is async. This component relies on async logic to be resolved
|
||||||
|
// before proceeding
|
||||||
|
if (instance.asyncDep) {
|
||||||
|
const suspense = (instance as any).provides.suspense
|
||||||
|
if (!suspense) {
|
||||||
|
throw new Error('Async component without a suspense boundary!')
|
||||||
|
}
|
||||||
|
suspense.deps++
|
||||||
|
instance.asyncDep.then(res => {
|
||||||
|
instance.asyncResolved = true
|
||||||
|
handleSetupResult(instance, res)
|
||||||
|
suspense.deps--
|
||||||
|
suspense.retry()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRenderEffect(instance, initialVNode, container, anchor, isSVG)
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
popWarningContext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRenderEffect(
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
initialVNode: HostVNode,
|
||||||
|
container: HostElement,
|
||||||
|
anchor: HostNode | null,
|
||||||
|
isSVG: boolean
|
||||||
|
) {
|
||||||
// create reactive effect for rendering
|
// create reactive effect for rendering
|
||||||
let mounted = false
|
let mounted = false
|
||||||
instance.update = effect(function componentEffect() {
|
instance.update = effect(function componentEffect() {
|
||||||
@ -751,7 +852,7 @@ export function createRenderer<
|
|||||||
next.component = instance
|
next.component = instance
|
||||||
instance.vnode = next
|
instance.vnode = next
|
||||||
instance.next = null
|
instance.next = null
|
||||||
resolveProps(instance, next.props, propsOptions)
|
resolveProps(instance, next.props, (initialVNode.type as any).props)
|
||||||
resolveSlots(instance, next.children)
|
resolveSlots(instance, next.children)
|
||||||
}
|
}
|
||||||
const prevTree = instance.subTree
|
const prevTree = instance.subTree
|
||||||
@ -797,10 +898,6 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
|
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
popWarningContext()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchChildren(
|
function patchChildren(
|
||||||
|
@ -19,7 +19,7 @@ export {
|
|||||||
createBlock
|
createBlock
|
||||||
} from './vnode'
|
} from './vnode'
|
||||||
// VNode type symbols
|
// VNode type symbols
|
||||||
export { Text, Empty, Fragment, Portal } from './vnode'
|
export { Text, Empty, Fragment, Portal, Suspense } from './vnode'
|
||||||
// VNode flags
|
// VNode flags
|
||||||
export { PublicPatchFlags as PatchFlags } from './patchFlags'
|
export { PublicPatchFlags as PatchFlags } from './patchFlags'
|
||||||
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
||||||
|
@ -1,46 +1,54 @@
|
|||||||
import { VNode } from './vnode'
|
import { VNode } from './vnode'
|
||||||
import { queuePostFlushCb } from './scheduler'
|
|
||||||
|
|
||||||
export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
|
export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
|
||||||
|
|
||||||
export interface SuspenseBoundary<HostNode, HostElement> {
|
export interface SuspenseBoundary<
|
||||||
|
HostNode,
|
||||||
|
HostElement,
|
||||||
|
HostVNode = VNode<HostNode, HostElement>
|
||||||
|
> {
|
||||||
|
vnode: HostVNode
|
||||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||||
contentTree: VNode<HostNode, HostElement> | null
|
contentTree: HostVNode | null
|
||||||
fallbackTree: VNode<HostNode, HostElement> | null
|
oldContentTree: HostVNode | null
|
||||||
|
fallbackTree: HostVNode | null
|
||||||
|
oldFallbackTree: HostVNode | null
|
||||||
deps: number
|
deps: number
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
bufferedJobs: Function[]
|
bufferedJobs: Function[]
|
||||||
container: HostElement
|
onRetry(fn: Function): void
|
||||||
|
retry(): void
|
||||||
|
onResolve(fn: Function): void
|
||||||
resolve(): void
|
resolve(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSuspenseBoundary<HostNode, HostElement>(
|
export function createSuspenseBoundary<HostNode, HostElement>(
|
||||||
parent: SuspenseBoundary<HostNode, HostElement> | null,
|
vnode: VNode<HostNode, HostElement>,
|
||||||
container: HostElement
|
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||||
): SuspenseBoundary<HostNode, HostElement> {
|
): SuspenseBoundary<HostNode, HostElement> {
|
||||||
|
let retry: Function
|
||||||
|
let resolve: Function
|
||||||
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
||||||
|
vnode,
|
||||||
parent,
|
parent,
|
||||||
container,
|
|
||||||
deps: 0,
|
deps: 0,
|
||||||
contentTree: null,
|
contentTree: null,
|
||||||
|
oldContentTree: null,
|
||||||
fallbackTree: null,
|
fallbackTree: null,
|
||||||
|
oldFallbackTree: null,
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
bufferedJobs: [],
|
bufferedJobs: [],
|
||||||
|
onRetry(fn: Function) {
|
||||||
|
retry = fn
|
||||||
|
},
|
||||||
|
retry() {
|
||||||
|
retry()
|
||||||
|
},
|
||||||
|
onResolve(fn: Function) {
|
||||||
|
resolve = fn
|
||||||
|
},
|
||||||
resolve() {
|
resolve() {
|
||||||
suspense.isResolved = true
|
resolve()
|
||||||
let parent = suspense.parent
|
|
||||||
let hasUnresolvedAncestor = false
|
|
||||||
while (parent) {
|
|
||||||
if (!parent.isResolved) {
|
|
||||||
parent.bufferedJobs.push(...suspense.bufferedJobs)
|
|
||||||
hasUnresolvedAncestor = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasUnresolvedAncestor) {
|
|
||||||
queuePostFlushCb(suspense.bufferedJobs)
|
|
||||||
}
|
|
||||||
suspense.isResolved = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,12 @@ export const nodeOps = {
|
|||||||
el.textContent = text
|
el.textContent = text
|
||||||
},
|
},
|
||||||
|
|
||||||
parentNode: (node: Node): Node | null => node.parentNode,
|
parentNode: (node: Node): Node | null => {
|
||||||
|
if (!node) {
|
||||||
|
debugger
|
||||||
|
}
|
||||||
|
return node.parentNode
|
||||||
|
},
|
||||||
|
|
||||||
nextSibling: (node: Node): Node | null => node.nextSibling,
|
nextSibling: (node: Node): Node | null => node.nextSibling,
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user