import { VNode, normalizeVNode, VNodeChild } from './vnode'
import { ReactiveEffect } from '@vue/observer'
-import { isFunction } from '@vue/shared'
+import { isFunction, EMPTY_OBJ } from '@vue/shared'
import { resolveProps, ComponentPropsOptions } from './componentProps'
interface Value<T> {
return options as any
}
-export type ComponentHandle = {
+type LifecycleHook = Function[] | null
+
+export interface LifecycleHooks {
+ bm: LifecycleHook // beforeMount
+ m: LifecycleHook // mounted
+ bu: LifecycleHook // beforeUpdate
+ u: LifecycleHook // updated
+ bum: LifecycleHook // beforeUnmount
+ um: LifecycleHook // unmounted
+ da: LifecycleHook // deactivated
+ a: LifecycleHook // activated
+ rtg: LifecycleHook // renderTriggered
+ rtc: LifecycleHook // renderTracked
+ ec: LifecycleHook // errorCaptured
+}
+
+export type ComponentInstance = {
type: FunctionalComponent | ComponentOptions
vnode: VNode | null
next: VNode | null
subTree: VNode | null
update: ReactiveEffect
-} & ComponentPublicProperties
+ bindings: Data | null
+ proxy: Data | null
+} & LifecycleHooks &
+ ComponentPublicProperties
+
+export function createComponentInstance(vnode: VNode): ComponentInstance {
+ const type = vnode.type as any
+ const instance = {
+ type,
+ vnode: null,
+ next: null,
+ subTree: null,
+ update: null as any,
+ bindings: null,
+ proxy: null,
+
+ bm: null,
+ m: null,
+ bu: null,
+ u: null,
+ um: null,
+ bum: null,
+ da: null,
+ a: null,
+ rtg: null,
+ rtc: null,
+ ec: null,
+
+ // public properties
+ $attrs: EMPTY_OBJ,
+ $props: EMPTY_OBJ,
+ $refs: EMPTY_OBJ,
+ $slots: EMPTY_OBJ,
+ $state: EMPTY_OBJ
+ }
+ if (typeof type === 'object' && type.setup) {
+ setupStatefulComponent(instance)
+ }
+ return instance
+}
+
+export let currentInstance: ComponentInstance | null = null
+
+const RenderProxyHandlers = {}
+
+export function setupStatefulComponent(instance: ComponentInstance) {
+ // 1. create render proxy
+ const proxy = (instance.proxy = new Proxy(instance, RenderProxyHandlers))
+ // 2. resolve initial props
+ // 3. call setup()
+ const type = instance.type as ComponentOptions
+ if (type.setup) {
+ currentInstance = instance
+ instance.bindings = type.setup.call(proxy, proxy)
+ currentInstance = null
+ }
+}
-export function renderComponentRoot(handle: ComponentHandle): VNode {
- const { type, vnode } = handle
+export function renderComponentRoot(instance: ComponentInstance): VNode {
+ const { type, vnode, proxy, $state, $slots } = instance
+ if (!type) debugger
const { 0: props, 1: attrs } = resolveProps(
(vnode as VNode).props,
type.props
)
const renderArg = {
- state: handle.$state,
- slots: handle.$slots,
+ state: $state,
+ slots: $slots,
props,
attrs
}
if (__DEV__ && !type.render) {
// TODO warn missing render
}
- return normalizeVNode((type.render as Function)(renderArg))
+ return normalizeVNode((type.render as Function).call(proxy, renderArg))
}
}
--- /dev/null
+import { ComponentInstance, LifecycleHooks, currentInstance } from './component'
+
+function injectHook(
+ name: keyof LifecycleHooks,
+ hook: () => void,
+ target: ComponentInstance | null | void = currentInstance
+) {
+ if (target) {
+ const existing = target[name]
+ if (existing !== null) {
+ existing.push(hook)
+ } else {
+ target[name] = [hook]
+ }
+ } else {
+ // TODO warn
+ }
+}
+
+export function onBeforeMount(hook: () => void, target?: ComponentInstance) {
+ injectHook('bm', hook, target)
+}
+
+export function onMounted(hook: () => void, target?: ComponentInstance) {
+ injectHook('m', hook, target)
+}
+
+export function onBeforeUpdate(hook: () => void, target?: ComponentInstance) {
+ injectHook('bu', hook, target)
+}
+
+export function onUpdated(hook: () => void, target?: ComponentInstance) {
+ injectHook('u', hook, target)
+}
+
+export function onBeforeUnmount(hook: () => void, target?: ComponentInstance) {
+ injectHook('bum', hook, target)
+}
+
+export function onUnmounted(hook: () => void, target?: ComponentInstance) {
+ injectHook('um', hook, target)
+}
+
+export function onRenderTriggered(
+ hook: () => void,
+ target?: ComponentInstance
+) {
+ injectHook('rtg', hook, target)
+}
+
+export function onRenderTracked(hook: () => void, target?: ComponentInstance) {
+ injectHook('rtc', hook, target)
+}
+
+export function onErrorCaptured(hook: () => void, target?: ComponentInstance) {
+ injectHook('ec', hook, target)
+}
isObject
} from '@vue/shared'
import { warn } from './warning'
-import { Data, ComponentHandle } from './component'
+import { Data, ComponentInstance } from './component'
export type ComponentPropsOptions<P = Data> = {
[K in keyof P]: PropValidator<P[K]>
const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
export function initializeProps(
- instance: ComponentHandle,
+ instance: ComponentInstance,
options: NormalizedPropsOptions | undefined,
data: Data | null
) {
// TODO:
-// - component
// - lifecycle / refs
+// - slots
// - keep alive
// - app context
// - svg
// - hydration
+// - error handling
// - warning context
// - parent chain
// - reused nodes (warning)
import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
import { effect, stop } from '@vue/observer'
import {
- ComponentHandle,
+ ComponentInstance,
renderComponentRoot,
- shouldUpdateComponent
+ shouldUpdateComponent,
+ createComponentInstance
} from './component'
-import { queueJob } from './scheduler'
+import {
+ queueJob,
+ queuePostFlushCb,
+ flushPostFlushCbs,
+ queueReversePostFlushCb
+} from './scheduler'
function isSameType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
if (n1 == null) {
mountComponent(n2, container, anchor)
} else {
- const instance = (n2.component = n1.component) as ComponentHandle
+ const instance = (n2.component = n1.component) as ComponentInstance
if (shouldUpdateComponent(n1, n2)) {
instance.next = n2
instance.update()
container: HostNode,
anchor?: HostNode
) {
- const instance: ComponentHandle = (vnode.component = {
- type: vnode.type as any,
- vnode: null,
- next: null,
- subTree: null,
- update: null as any,
- $attrs: EMPTY_OBJ,
- $props: EMPTY_OBJ,
- $refs: EMPTY_OBJ,
- $slots: EMPTY_OBJ,
- $state: EMPTY_OBJ
- })
-
- // TODO call setup, handle bindings and render context
-
+ const instance: ComponentInstance = (vnode.component = createComponentInstance(
+ vnode
+ ))
instance.update = effect(
() => {
if (!instance.vnode) {
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor)
vnode.el = subTree.el
+ // mounted hook
+ if (instance.m !== null) {
+ queuePostFlushCb(instance.m)
+ }
} else {
// this is triggered by processComponent with `next` already set
- updateComponent(instance)
+ const { next } = instance
+ if (next != null) {
+ next.component = instance
+ instance.vnode = next
+ instance.next = null
+ }
+ const prevTree = instance.subTree as VNode
+ const nextTree = (instance.subTree = renderComponentRoot(instance))
+ patch(
+ prevTree,
+ nextTree,
+ container || hostParentNode(prevTree.el),
+ anchor || getNextHostNode(prevTree)
+ )
+ if (next != null) {
+ next.el = nextTree.el
+ }
+ // upated hook
+ if (instance.u !== null) {
+ // updated hooks are queued top-down, but should be fired bottom up
+ queueReversePostFlushCb(instance.u)
+ }
}
},
{
)
}
- function updateComponent(
- instance: any,
- container?: HostNode,
- anchor?: HostNode
- ) {
- const { next: vnode } = instance
- if (vnode != null) {
- vnode.component = instance
- instance.vnode = vnode
- instance.next = null
- }
- const prevTree = instance.subTree
- const nextTree = (instance.subTree = renderComponentRoot(instance))
- patch(
- prevTree,
- nextTree,
- container || hostParentNode(prevTree.el),
- anchor || getNextHostNode(prevTree)
- )
- if (vnode != null) {
- vnode.el = nextTree.el
- }
- }
-
function patchChildren(
n1: VNode | null,
n2: VNode,
}
function unmount(vnode: VNode, doRemove?: boolean) {
- if (vnode.component != null) {
+ const instance = vnode.component
+ if (instance != null) {
// TODO teardown component
- stop(vnode.component.update)
- unmount(vnode.component.subTree as VNode, doRemove)
+ stop(instance.update)
+ unmount(instance.subTree as VNode, doRemove)
+ if (instance.um !== null) {
+ queuePostFlushCb(instance.um)
+ }
return
}
const shouldRemoveChildren = vnode.type === Fragment && doRemove
return function render(vnode: VNode, dom: HostNode): VNode {
patch(dom._vnode, vnode, dom)
+ flushPostFlushCbs()
return (dom._vnode = vnode)
}
}
createComponent
} from './component'
+export * from './componentLifecycle'
+
export { createRenderer, RendererOptions } from './createRenderer'
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
export * from '@vue/observer'
-const queue: Array<() => void> = []
-const postFlushCbs: Array<() => void> = []
+const queue: Function[] = []
+const postFlushCbs: Function[] = []
+const reversePostFlushCbs: Function[] = []
const p = Promise.resolve()
let isFlushing = false
}
}
-export function queuePostFlushCb(cb: () => void) {
- if (postFlushCbs.indexOf(cb) === -1) {
- postFlushCbs.push(cb)
+export function queuePostFlushCb(cb: Function | Function[]) {
+ queuePostCb(cb, postFlushCbs)
+}
+
+export function queueReversePostFlushCb(cb: Function | Function[]) {
+ queuePostCb(cb, reversePostFlushCbs)
+}
+
+function queuePostCb(cb: Function | Function[], queue: Function[]) {
+ if (Array.isArray(cb)) {
+ queue.push.apply(postFlushCbs, cb)
+ } else {
+ queue.push(cb)
}
}
+const dedupe = (cbs: Function[]): Function[] => Array.from(new Set(cbs))
+
export function flushPostFlushCbs() {
- const cbs = postFlushCbs.slice()
- let i = cbs.length
- postFlushCbs.length = 0
- // post flush cbs are flushed in reverse since they are queued top-down
- // but should fire bottom-up
- while (i--) {
- cbs[i]()
+ if (reversePostFlushCbs.length) {
+ const cbs = dedupe(reversePostFlushCbs)
+ reversePostFlushCbs.length = 0
+ let i = cbs.length
+ while (i--) {
+ cbs[i]()
+ }
+ }
+ if (postFlushCbs.length) {
+ const cbs = dedupe(postFlushCbs)
+ postFlushCbs.length = 0
+ for (let i = 0; i < cbs.length; i++) {
+ cbs[i]()
+ }
}
}
import { isArray, isFunction } from '@vue/shared'
-import { ComponentHandle } from './component'
+import { ComponentInstance } from './component'
import { HostNode } from './createRenderer'
export const Fragment = Symbol('Fragment')
props: { [key: string]: any } | null
key: string | number | null
children: string | VNodeChildren | null
- component: ComponentHandle | null
+ component: ComponentInstance | null
// DOM
el: HostNode | null