wip: somewhat working suspense

This commit is contained in:
Evan You 2019-09-09 16:00:50 -04:00
parent 1dc9d81e3e
commit 02bb156314
5 changed files with 216 additions and 84 deletions

View File

@ -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

View File

@ -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(

View File

@ -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'

View File

@ -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
} }
} }

View File

@ -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,