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
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: {},
}
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
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
import {
ComponentInternalInstance,
createComponentInstance,
- setupStatefulComponent
+ setupStatefulComponent,
+ handleSetupResult,
+ setCurrentInstance
} from './component'
import {
renderComponentRoot,
import { ComponentPublicInstance } from './componentPublicInstanceProxy'
import { App, createAppAPI } from './apiApp'
import { SuspenseBoundary, createSuspenseBoundary } from './suspense'
+import { provide } from './apiInject'
const prodEffectOptions = {
scheduler: queueJob
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,
// 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()
}
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
}
}
}
} 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
}
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() {
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
}
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
-
- if (__DEV__) {
- popWarningContext()
- }
}
function patchChildren(
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'
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()
}
}
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,