From: Evan You Date: Tue, 28 May 2019 11:36:15 +0000 (+0800) Subject: wip: lifecycle hooks X-Git-Tag: v3.0.0-alpha.0~991 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=19ed750078e8ea6450036ee5c811dc9156a28c02;p=thirdparty%2Fvuejs%2Fcore.git wip: lifecycle hooks --- diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a37355dcd8..52690bc5f2 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1,6 +1,6 @@ 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 { @@ -79,23 +79,96 @@ export function createComponent< 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 } @@ -105,7 +178,7 @@ export function renderComponentRoot(handle: ComponentHandle): VNode { if (__DEV__ && !type.render) { // TODO warn missing render } - return normalizeVNode((type.render as Function)(renderArg)) + return normalizeVNode((type.render as Function).call(proxy, renderArg)) } } diff --git a/packages/runtime-core/src/componentLifecycle.ts b/packages/runtime-core/src/componentLifecycle.ts new file mode 100644 index 0000000000..29827a5e09 --- /dev/null +++ b/packages/runtime-core/src/componentLifecycle.ts @@ -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) +} diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 1544c1ebc6..da0405ea8a 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -10,7 +10,7 @@ import { isObject } from '@vue/shared' import { warn } from './warning' -import { Data, ComponentHandle } from './component' +import { Data, ComponentInstance } from './component' export type ComponentPropsOptions

= { [K in keyof P]: PropValidator @@ -44,7 +44,7 @@ type NormalizedPropsOptions = Record const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$' export function initializeProps( - instance: ComponentHandle, + instance: ComponentInstance, options: NormalizedPropsOptions | undefined, data: Data | null ) { diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 169ad0d9be..2b381e431e 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -1,10 +1,11 @@ // TODO: -// - component // - lifecycle / refs +// - slots // - keep alive // - app context // - svg // - hydration +// - error handling // - warning context // - parent chain // - 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 { 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 @@ -333,7 +340,7 @@ export function createRenderer(options: RendererOptions) { 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() @@ -348,21 +355,9 @@ export function createRenderer(options: RendererOptions) { 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) { @@ -371,9 +366,34 @@ export function createRenderer(options: RendererOptions) { 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) + } } }, { @@ -382,30 +402,6 @@ export function createRenderer(options: RendererOptions) { ) } - 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, @@ -681,10 +677,14 @@ export function createRenderer(options: RendererOptions) { } 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 @@ -717,6 +717,7 @@ export function createRenderer(options: RendererOptions) { return function render(vnode: VNode, dom: HostNode): VNode { patch(dom._vnode, vnode, dom) + flushPostFlushCbs() return (dom._vnode = vnode) } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 92850f9483..d212e935f7 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -16,6 +16,8 @@ export { createComponent } from './component' +export * from './componentLifecycle' + export { createRenderer, RendererOptions } from './createRenderer' export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' export * from '@vue/observer' diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 1652fd5550..bf37774c35 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,5 +1,6 @@ -const queue: Array<() => void> = [] -const postFlushCbs: Array<() => void> = [] +const queue: Function[] = [] +const postFlushCbs: Function[] = [] +const reversePostFlushCbs: Function[] = [] const p = Promise.resolve() let isFlushing = false @@ -18,20 +19,39 @@ export function queueJob(job: () => void, onError?: (err: Error) => void) { } } -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]() + } } } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 3fd8743595..8e5afa4b74 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -1,5 +1,5 @@ import { isArray, isFunction } from '@vue/shared' -import { ComponentHandle } from './component' +import { ComponentInstance } from './component' import { HostNode } from './createRenderer' export const Fragment = Symbol('Fragment') @@ -24,7 +24,7 @@ export interface VNode { props: { [key: string]: any } | null key: string | number | null children: string | VNodeChildren | null - component: ComponentHandle | null + component: ComponentInstance | null // DOM el: HostNode | null