wip: lifecycle hooks
This commit is contained in:
parent
9dd133b1e9
commit
19ed750078
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
packages/runtime-core/src/componentLifecycle.ts
Normal file
57
packages/runtime-core/src/componentLifecycle.ts
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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,21 +19,40 @@ 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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user