wip: somewhat working suspense
This commit is contained in:
parent
1dc9d81e3e
commit
02bb156314
@ -78,6 +78,10 @@ export interface ComponentInternalInstance {
|
||||
components: Record<string, Component>
|
||||
directives: Record<string, Directive>
|
||||
|
||||
asyncDep: Promise<any> | null
|
||||
asyncResult: any
|
||||
asyncResolved: boolean
|
||||
|
||||
// the rest are only for stateful components
|
||||
renderContext: Data
|
||||
data: Data
|
||||
@ -146,6 +150,11 @@ export function createComponentInstance(
|
||||
components: Object.create(appContext.components),
|
||||
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: {},
|
||||
|
||||
@ -206,7 +215,6 @@ export const setCurrentInstance = (
|
||||
}
|
||||
|
||||
export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
||||
currentInstance = instance
|
||||
const Component = instance.type as ComponentOptions
|
||||
// 1. create render proxy
|
||||
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
|
||||
@ -219,62 +227,76 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
||||
if (setup) {
|
||||
const setupContext = (instance.setupContext =
|
||||
setup.length > 1 ? createSetupContext(instance) : null)
|
||||
|
||||
currentInstance = instance
|
||||
const setupResult = callWithErrorHandling(
|
||||
setup,
|
||||
instance,
|
||||
ErrorCodes.SETUP_FUNCTION,
|
||||
[propsProxy, setupContext]
|
||||
)
|
||||
currentInstance = null
|
||||
|
||||
if (isFunction(setupResult)) {
|
||||
// setup returned an inline render function
|
||||
instance.render = setupResult
|
||||
if (
|
||||
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 {
|
||||
if (__DEV__) {
|
||||
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
|
||||
handleSetupResult(instance, setupResult)
|
||||
}
|
||||
} 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) {
|
||||
warn(
|
||||
`Component is missing render function. Either provide a template or ` +
|
||||
`return a render function from setup().`
|
||||
)
|
||||
}
|
||||
instance.render = Component.render as RenderFunction
|
||||
instance.render = (Component.render || NOOP) as RenderFunction
|
||||
}
|
||||
|
||||
// support for 2.x options
|
||||
if (__FEATURE_OPTIONS__) {
|
||||
currentInstance = instance
|
||||
applyOptions(instance, Component)
|
||||
currentInstance = null
|
||||
}
|
||||
|
||||
if (instance.renderContext === EMPTY_OBJ) {
|
||||
instance.renderContext = reactive({})
|
||||
}
|
||||
currentInstance = null
|
||||
}
|
||||
|
||||
// used to identify a setup context proxy
|
||||
|
@ -12,7 +12,9 @@ import {
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
createComponentInstance,
|
||||
setupStatefulComponent
|
||||
setupStatefulComponent,
|
||||
handleSetupResult,
|
||||
setCurrentInstance
|
||||
} from './component'
|
||||
import {
|
||||
renderComponentRoot,
|
||||
@ -43,6 +45,7 @@ import { invokeDirectiveHook } from './directives'
|
||||
import { ComponentPublicInstance } from './componentPublicInstanceProxy'
|
||||
import { App, createAppAPI } from './apiApp'
|
||||
import { SuspenseBoundary, createSuspenseBoundary } from './suspense'
|
||||
import { provide } from './apiInject'
|
||||
|
||||
const prodEffectOptions = {
|
||||
scheduler: queueJob
|
||||
@ -604,16 +607,68 @@ export function createRenderer<
|
||||
if (n1 == null) {
|
||||
const contentContainer = hostCreateElement('div')
|
||||
const suspense = (n2.suspense = createSuspenseBoundary(
|
||||
parentSuspense,
|
||||
contentContainer
|
||||
n2,
|
||||
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
|
||||
// - TODO tracking async deps and buffering postQueue jobs on current boundary
|
||||
const contentTree = (suspense.contentTree = childrenToFragment(n2))
|
||||
// TODO should buffer postQueue jobs on current boundary
|
||||
const contentTree = (suspense.contentTree = suspense.oldContentTree = childrenToFragment(
|
||||
n2
|
||||
))
|
||||
processFragment(
|
||||
null,
|
||||
contentTree as VNode<HostNode, HostElement>,
|
||||
contentTree as HostVNode,
|
||||
contentContainer,
|
||||
null,
|
||||
parentComponent,
|
||||
@ -625,6 +680,7 @@ export function createRenderer<
|
||||
// yes: mount the fallback tree.
|
||||
// Each time an async dep resolves, it pings the boundary
|
||||
// and causes a re-entry.
|
||||
console.log('fallback')
|
||||
} else {
|
||||
suspense.resolve()
|
||||
}
|
||||
@ -633,23 +689,23 @@ export function createRenderer<
|
||||
HostNode,
|
||||
HostElement
|
||||
>
|
||||
const oldContentTree = suspense.contentTree
|
||||
suspense.vnode = n2
|
||||
const oldContentTree = (suspense.oldContentTree = suspense.contentTree)
|
||||
const newContentTree = (suspense.contentTree = childrenToFragment(n2))
|
||||
// patch suspense subTree as fragment
|
||||
processFragment(
|
||||
oldContentTree,
|
||||
newContentTree,
|
||||
container,
|
||||
anchor,
|
||||
parentComponent,
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
if (suspense.deps > 0) {
|
||||
// still pending.
|
||||
// patch the fallback tree.
|
||||
if (!suspense.isResolved) {
|
||||
suspense.retry()
|
||||
} 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 {
|
||||
const instance = (n2.component =
|
||||
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.update()
|
||||
} else {
|
||||
// no update needed. just copy over properties
|
||||
n2.component = n1.component
|
||||
n2.el = n1.el
|
||||
}
|
||||
@ -720,6 +790,37 @@ export function createRenderer<
|
||||
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
|
||||
let mounted = false
|
||||
instance.update = effect(function componentEffect() {
|
||||
@ -751,7 +852,7 @@ export function createRenderer<
|
||||
next.component = instance
|
||||
instance.vnode = next
|
||||
instance.next = null
|
||||
resolveProps(instance, next.props, propsOptions)
|
||||
resolveProps(instance, next.props, (initialVNode.type as any).props)
|
||||
resolveSlots(instance, next.children)
|
||||
}
|
||||
const prevTree = instance.subTree
|
||||
@ -797,10 +898,6 @@ export function createRenderer<
|
||||
}
|
||||
}
|
||||
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
|
||||
|
||||
if (__DEV__) {
|
||||
popWarningContext()
|
||||
}
|
||||
}
|
||||
|
||||
function patchChildren(
|
||||
|
@ -19,7 +19,7 @@ export {
|
||||
createBlock
|
||||
} from './vnode'
|
||||
// VNode type symbols
|
||||
export { Text, Empty, Fragment, Portal } from './vnode'
|
||||
export { Text, Empty, Fragment, Portal, Suspense } from './vnode'
|
||||
// VNode flags
|
||||
export { PublicPatchFlags as PatchFlags } from './patchFlags'
|
||||
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
||||
|
@ -1,46 +1,54 @@
|
||||
import { VNode } from './vnode'
|
||||
import { queuePostFlushCb } from './scheduler'
|
||||
|
||||
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
|
||||
contentTree: VNode<HostNode, HostElement> | null
|
||||
fallbackTree: VNode<HostNode, HostElement> | null
|
||||
contentTree: HostVNode | null
|
||||
oldContentTree: HostVNode | null
|
||||
fallbackTree: HostVNode | null
|
||||
oldFallbackTree: HostVNode | null
|
||||
deps: number
|
||||
isResolved: boolean
|
||||
bufferedJobs: Function[]
|
||||
container: HostElement
|
||||
onRetry(fn: Function): void
|
||||
retry(): void
|
||||
onResolve(fn: Function): void
|
||||
resolve(): void
|
||||
}
|
||||
|
||||
export function createSuspenseBoundary<HostNode, HostElement>(
|
||||
parent: SuspenseBoundary<HostNode, HostElement> | null,
|
||||
container: HostElement
|
||||
vnode: VNode<HostNode, HostElement>,
|
||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||
): SuspenseBoundary<HostNode, HostElement> {
|
||||
let retry: Function
|
||||
let resolve: Function
|
||||
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
||||
vnode,
|
||||
parent,
|
||||
container,
|
||||
deps: 0,
|
||||
contentTree: null,
|
||||
oldContentTree: null,
|
||||
fallbackTree: null,
|
||||
oldFallbackTree: null,
|
||||
isResolved: false,
|
||||
bufferedJobs: [],
|
||||
onRetry(fn: Function) {
|
||||
retry = fn
|
||||
},
|
||||
retry() {
|
||||
retry()
|
||||
},
|
||||
onResolve(fn: Function) {
|
||||
resolve = fn
|
||||
},
|
||||
resolve() {
|
||||
suspense.isResolved = true
|
||||
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
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,12 @@ export const nodeOps = {
|
||||
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,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user