]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: lifecycle hooks
authorEvan You <yyx990803@gmail.com>
Tue, 28 May 2019 11:36:15 +0000 (19:36 +0800)
committerEvan You <yyx990803@gmail.com>
Tue, 28 May 2019 11:36:15 +0000 (19:36 +0800)
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentLifecycle.ts [new file with mode: 0644]
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/scheduler.ts
packages/runtime-core/src/vnode.ts

index a37355dcd83d25901b748d28a15cf59e87ef9f42..52690bc5f2d0795547d409b4d9b0fb55a35af8e2 100644 (file)
@@ -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<T> {
@@ -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 (file)
index 0000000..29827a5
--- /dev/null
@@ -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)
+}
index 1544c1ebc6b739601f13a54ba7faa0b1c0647efc..da0405ea8a346d383fc0d8dbb3a8844f7a112857 100644 (file)
@@ -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<P = Data> = {
   [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] === '$'
 
 export function initializeProps(
-  instance: ComponentHandle,
+  instance: ComponentInstance,
   options: NormalizedPropsOptions | undefined,
   data: Data | null
 ) {
index 169ad0d9be25798f1d2c810091ec8afe174b4be9..2b381e431ea429bbc3a07ee8a4b1f359cf497dc9 100644 (file)
@@ -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)
   }
 }
index 92850f94831a5f5bbfea80a5c7dddfca70584b67..d212e935f7a5dd50d2864ebcd150827a66bde7de 100644 (file)
@@ -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'
index 1652fd5550518635d21831eaaf9521f4fb33505c..bf37774c35a45f802325ef81104ee239d6f60954 100644 (file)
@@ -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]()
+    }
   }
 }
 
index 3fd8743595531541b53e4ef89aaeb951ce513ff3..8e5afa4b74a97401c04d5d8539cbf28af72c5b96 100644 (file)
@@ -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