wip: lifecycle hooks

This commit is contained in:
Evan You 2019-05-28 19:36:15 +08:00
parent 9dd133b1e9
commit 19ed750078
7 changed files with 225 additions and 72 deletions

View File

@ -1,6 +1,6 @@
import { VNode, normalizeVNode, VNodeChild } from './vnode' import { VNode, normalizeVNode, VNodeChild } from './vnode'
import { ReactiveEffect } from '@vue/observer' import { ReactiveEffect } from '@vue/observer'
import { isFunction } from '@vue/shared' import { isFunction, EMPTY_OBJ } from '@vue/shared'
import { resolveProps, ComponentPropsOptions } from './componentProps' import { resolveProps, ComponentPropsOptions } from './componentProps'
interface Value<T> { interface Value<T> {
@ -79,23 +79,96 @@ export function createComponent<
return options as any 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 type: FunctionalComponent | ComponentOptions
vnode: VNode | null vnode: VNode | null
next: VNode | null next: VNode | null
subTree: VNode | null subTree: VNode | null
update: ReactiveEffect update: ReactiveEffect
} & ComponentPublicProperties bindings: Data | null
proxy: Data | null
} & LifecycleHooks &
ComponentPublicProperties
export function renderComponentRoot(handle: ComponentHandle): VNode { export function createComponentInstance(vnode: VNode): ComponentInstance {
const { type, vnode } = handle 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(instance: ComponentInstance): VNode {
const { type, vnode, proxy, $state, $slots } = instance
if (!type) debugger
const { 0: props, 1: attrs } = resolveProps( const { 0: props, 1: attrs } = resolveProps(
(vnode as VNode).props, (vnode as VNode).props,
type.props type.props
) )
const renderArg = { const renderArg = {
state: handle.$state, state: $state,
slots: handle.$slots, slots: $slots,
props, props,
attrs attrs
} }
@ -105,7 +178,7 @@ export function renderComponentRoot(handle: ComponentHandle): VNode {
if (__DEV__ && !type.render) { if (__DEV__ && !type.render) {
// TODO warn missing render // TODO warn missing render
} }
return normalizeVNode((type.render as Function)(renderArg)) return normalizeVNode((type.render as Function).call(proxy, renderArg))
} }
} }

View File

@ -0,0 +1,57 @@
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)
}

View File

@ -10,7 +10,7 @@ import {
isObject isObject
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { Data, ComponentHandle } from './component' import { Data, ComponentInstance } from './component'
export type ComponentPropsOptions<P = Data> = { export type ComponentPropsOptions<P = Data> = {
[K in keyof P]: PropValidator<P[K]> [K in keyof P]: PropValidator<P[K]>
@ -44,7 +44,7 @@ type NormalizedPropsOptions = Record<string, NormalizedProp>
const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$' const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
export function initializeProps( export function initializeProps(
instance: ComponentHandle, instance: ComponentInstance,
options: NormalizedPropsOptions | undefined, options: NormalizedPropsOptions | undefined,
data: Data | null data: Data | null
) { ) {

View File

@ -1,10 +1,11 @@
// TODO: // TODO:
// - component
// - lifecycle / refs // - lifecycle / refs
// - slots
// - keep alive // - keep alive
// - app context // - app context
// - svg // - svg
// - hydration // - hydration
// - error handling
// - warning context // - warning context
// - parent chain // - parent chain
// - reused nodes (warning) // - reused nodes (warning)
@ -22,11 +23,17 @@ import { isString, isArray, EMPTY_OBJ, EMPTY_ARR } from '@vue/shared'
import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
import { effect, stop } from '@vue/observer' import { effect, stop } from '@vue/observer'
import { import {
ComponentHandle, ComponentInstance,
renderComponentRoot, renderComponentRoot,
shouldUpdateComponent shouldUpdateComponent,
createComponentInstance
} from './component' } from './component'
import { queueJob } from './scheduler' import {
queueJob,
queuePostFlushCb,
flushPostFlushCbs,
queueReversePostFlushCb
} from './scheduler'
function isSameType(n1: VNode, n2: VNode): boolean { function isSameType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key return n1.type === n2.type && n1.key === n2.key
@ -333,7 +340,7 @@ export function createRenderer(options: RendererOptions) {
if (n1 == null) { if (n1 == null) {
mountComponent(n2, container, anchor) mountComponent(n2, container, anchor)
} else { } else {
const instance = (n2.component = n1.component) as ComponentHandle const instance = (n2.component = n1.component) as ComponentInstance
if (shouldUpdateComponent(n1, n2)) { if (shouldUpdateComponent(n1, n2)) {
instance.next = n2 instance.next = n2
instance.update() instance.update()
@ -348,21 +355,9 @@ export function createRenderer(options: RendererOptions) {
container: HostNode, container: HostNode,
anchor?: HostNode anchor?: HostNode
) { ) {
const instance: ComponentHandle = (vnode.component = { const instance: ComponentInstance = (vnode.component = createComponentInstance(
type: vnode.type as any, vnode
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
instance.update = effect( instance.update = effect(
() => { () => {
if (!instance.vnode) { if (!instance.vnode) {
@ -371,29 +366,19 @@ export function createRenderer(options: RendererOptions) {
const subTree = (instance.subTree = renderComponentRoot(instance)) const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor) patch(null, subTree, container, anchor)
vnode.el = subTree.el vnode.el = subTree.el
// mounted hook
if (instance.m !== null) {
queuePostFlushCb(instance.m)
}
} else { } else {
// this is triggered by processComponent with `next` already set // this is triggered by processComponent with `next` already set
updateComponent(instance) const { next } = instance
} if (next != null) {
}, next.component = instance
{ instance.vnode = next
scheduler: queueJob
}
)
}
function updateComponent(
instance: any,
container?: HostNode,
anchor?: HostNode
) {
const { next: vnode } = instance
if (vnode != null) {
vnode.component = instance
instance.vnode = vnode
instance.next = null instance.next = null
} }
const prevTree = instance.subTree const prevTree = instance.subTree as VNode
const nextTree = (instance.subTree = renderComponentRoot(instance)) const nextTree = (instance.subTree = renderComponentRoot(instance))
patch( patch(
prevTree, prevTree,
@ -401,9 +386,20 @@ export function createRenderer(options: RendererOptions) {
container || hostParentNode(prevTree.el), container || hostParentNode(prevTree.el),
anchor || getNextHostNode(prevTree) anchor || getNextHostNode(prevTree)
) )
if (vnode != null) { if (next != null) {
vnode.el = nextTree.el 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)
}
}
},
{
scheduler: queueJob
}
)
} }
function patchChildren( function patchChildren(
@ -681,10 +677,14 @@ export function createRenderer(options: RendererOptions) {
} }
function unmount(vnode: VNode, doRemove?: boolean) { function unmount(vnode: VNode, doRemove?: boolean) {
if (vnode.component != null) { const instance = vnode.component
if (instance != null) {
// TODO teardown component // TODO teardown component
stop(vnode.component.update) stop(instance.update)
unmount(vnode.component.subTree as VNode, doRemove) unmount(instance.subTree as VNode, doRemove)
if (instance.um !== null) {
queuePostFlushCb(instance.um)
}
return return
} }
const shouldRemoveChildren = vnode.type === Fragment && doRemove const shouldRemoveChildren = vnode.type === Fragment && doRemove
@ -717,6 +717,7 @@ export function createRenderer(options: RendererOptions) {
return function render(vnode: VNode, dom: HostNode): VNode { return function render(vnode: VNode, dom: HostNode): VNode {
patch(dom._vnode, vnode, dom) patch(dom._vnode, vnode, dom)
flushPostFlushCbs()
return (dom._vnode = vnode) return (dom._vnode = vnode)
} }
} }

View File

@ -16,6 +16,8 @@ export {
createComponent createComponent
} from './component' } from './component'
export * from './componentLifecycle'
export { createRenderer, RendererOptions } from './createRenderer' export { createRenderer, RendererOptions } from './createRenderer'
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
export * from '@vue/observer' export * from '@vue/observer'

View File

@ -1,5 +1,6 @@
const queue: Array<() => void> = [] const queue: Function[] = []
const postFlushCbs: Array<() => void> = [] const postFlushCbs: Function[] = []
const reversePostFlushCbs: Function[] = []
const p = Promise.resolve() const p = Promise.resolve()
let isFlushing = false let isFlushing = false
@ -18,22 +19,41 @@ export function queueJob(job: () => void, onError?: (err: Error) => void) {
} }
} }
export function queuePostFlushCb(cb: () => void) { export function queuePostFlushCb(cb: Function | Function[]) {
if (postFlushCbs.indexOf(cb) === -1) { queuePostCb(cb, postFlushCbs)
postFlushCbs.push(cb) }
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() { export function flushPostFlushCbs() {
const cbs = postFlushCbs.slice() if (reversePostFlushCbs.length) {
const cbs = dedupe(reversePostFlushCbs)
reversePostFlushCbs.length = 0
let i = cbs.length 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--) { while (i--) {
cbs[i]() cbs[i]()
} }
} }
if (postFlushCbs.length) {
const cbs = dedupe(postFlushCbs)
postFlushCbs.length = 0
for (let i = 0; i < cbs.length; i++) {
cbs[i]()
}
}
}
const RECURSION_LIMIT = 100 const RECURSION_LIMIT = 100
type JobCountMap = Map<Function, number> type JobCountMap = Map<Function, number>

View File

@ -1,5 +1,5 @@
import { isArray, isFunction } from '@vue/shared' import { isArray, isFunction } from '@vue/shared'
import { ComponentHandle } from './component' import { ComponentInstance } from './component'
import { HostNode } from './createRenderer' import { HostNode } from './createRenderer'
export const Fragment = Symbol('Fragment') export const Fragment = Symbol('Fragment')
@ -24,7 +24,7 @@ export interface VNode {
props: { [key: string]: any } | null props: { [key: string]: any } | null
key: string | number | null key: string | number | null
children: string | VNodeChildren | null children: string | VNodeChildren | null
component: ComponentHandle | null component: ComponentInstance | null
// DOM // DOM
el: HostNode | null el: HostNode | null