diff --git a/packages/runtime-core/__tests__/apiLifecycle.spec.ts b/packages/runtime-core/__tests__/apiLifecycle.spec.ts
index dd0c8fba..8798b5ae 100644
--- a/packages/runtime-core/__tests__/apiLifecycle.spec.ts
+++ b/packages/runtime-core/__tests__/apiLifecycle.spec.ts
@@ -1,5 +1,318 @@
+import {
+ onBeforeMount,
+ h,
+ nodeOps,
+ render,
+ serializeInner,
+ onMounted,
+ ref,
+ onBeforeUpdate,
+ nextTick,
+ onUpdated,
+ onBeforeUnmount,
+ onUnmounted,
+ onRenderTracked,
+ reactive,
+ OperationTypes,
+ onRenderTriggered
+} from '@vue/runtime-test'
+import { ITERATE_KEY, DebuggerEvent } from '@vue/reactivity'
+
// reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
describe('api: lifecycle hooks', () => {
- test.todo('should work')
+ it('onBeforeMount', () => {
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called before inner div is rendered
+ expect(serializeInner(root)).toBe(``)
+ })
+
+ const Comp = {
+ setup() {
+ onBeforeMount(fn)
+ return () => h('div')
+ }
+ }
+ render(h(Comp), root)
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('onMounted', () => {
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called after inner div is rendered
+ expect(serializeInner(root)).toBe(`
`)
+ })
+
+ const Comp = {
+ setup() {
+ onMounted(fn)
+ return () => h('div')
+ }
+ }
+ render(h(Comp), root)
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('onBeforeUpdate', async () => {
+ const count = ref(0)
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called before inner div is updated
+ expect(serializeInner(root)).toBe(`0
`)
+ })
+
+ const Comp = {
+ setup() {
+ onBeforeUpdate(fn)
+ return () => h('div', count.value)
+ }
+ }
+ render(h(Comp), root)
+
+ count.value++
+ await nextTick()
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('onUpdated', async () => {
+ const count = ref(0)
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called after inner div is updated
+ expect(serializeInner(root)).toBe(`1
`)
+ })
+
+ const Comp = {
+ setup() {
+ onUpdated(fn)
+ return () => h('div', count.value)
+ }
+ }
+ render(h(Comp), root)
+
+ count.value++
+ await nextTick()
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('onBeforeUnmount', async () => {
+ const toggle = ref(true)
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called before inner div is removed
+ expect(serializeInner(root)).toBe(``)
+ })
+
+ const Comp = {
+ setup() {
+ return () => (toggle.value ? h(Child) : null)
+ }
+ }
+
+ const Child = {
+ setup() {
+ onBeforeUnmount(fn)
+ return () => h('div')
+ }
+ }
+
+ render(h(Comp), root)
+
+ toggle.value = false
+ await nextTick()
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('onUnmounted', async () => {
+ const toggle = ref(true)
+ const root = nodeOps.createElement('div')
+ const fn = jest.fn(() => {
+ // should be called after inner div is removed
+ expect(serializeInner(root)).toBe(``)
+ })
+
+ const Comp = {
+ setup() {
+ return () => (toggle.value ? h(Child) : null)
+ }
+ }
+
+ const Child = {
+ setup() {
+ onUnmounted(fn)
+ return () => h('div')
+ }
+ }
+
+ render(h(Comp), root)
+
+ toggle.value = false
+ await nextTick()
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('lifecycle call order', async () => {
+ const count = ref(0)
+ const root = nodeOps.createElement('div')
+ const calls: string[] = []
+
+ const Root = {
+ setup() {
+ onBeforeMount(() => calls.push('root onBeforeMount'))
+ onMounted(() => calls.push('root onMounted'))
+ onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
+ onUpdated(() => calls.push('root onUpdated'))
+ onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
+ onUnmounted(() => calls.push('root onUnmounted'))
+ return () => h(Mid, { count: count.value })
+ }
+ }
+
+ const Mid = {
+ setup(props: any) {
+ onBeforeMount(() => calls.push('mid onBeforeMount'))
+ onMounted(() => calls.push('mid onMounted'))
+ onBeforeUpdate(() => calls.push('mid onBeforeUpdate'))
+ onUpdated(() => calls.push('mid onUpdated'))
+ onBeforeUnmount(() => calls.push('mid onBeforeUnmount'))
+ onUnmounted(() => calls.push('mid onUnmounted'))
+ return () => h(Child, { count: props.count })
+ }
+ }
+
+ const Child = {
+ setup(props: any) {
+ onBeforeMount(() => calls.push('child onBeforeMount'))
+ onMounted(() => calls.push('child onMounted'))
+ onBeforeUpdate(() => calls.push('child onBeforeUpdate'))
+ onUpdated(() => calls.push('child onUpdated'))
+ onBeforeUnmount(() => calls.push('child onBeforeUnmount'))
+ onUnmounted(() => calls.push('child onUnmounted'))
+ return () => h('div', props.count)
+ }
+ }
+
+ // mount
+ render(h(Root), root)
+ expect(calls).toEqual([
+ 'root onBeforeMount',
+ 'mid onBeforeMount',
+ 'child onBeforeMount',
+ 'child onMounted',
+ 'mid onMounted',
+ 'root onMounted'
+ ])
+
+ calls.length = 0
+
+ // update
+ count.value++
+ await nextTick()
+ expect(calls).toEqual([
+ 'root onBeforeUpdate',
+ 'mid onBeforeUpdate',
+ 'child onBeforeUpdate',
+ 'child onUpdated',
+ 'mid onUpdated',
+ 'root onUpdated'
+ ])
+
+ calls.length = 0
+
+ // unmount
+ render(null, root)
+ expect(calls).toEqual([
+ 'root onBeforeUnmount',
+ 'mid onBeforeUnmount',
+ 'child onBeforeUnmount',
+ 'child onUnmounted',
+ 'mid onUnmounted',
+ 'root onUnmounted'
+ ])
+ })
+
+ it('onRenderTracked', () => {
+ const events: DebuggerEvent[] = []
+ const onTrack = jest.fn((e: DebuggerEvent) => {
+ events.push(e)
+ })
+ const obj = reactive({ foo: 1, bar: 2 })
+
+ const Comp = {
+ setup() {
+ onRenderTracked(onTrack)
+ return () =>
+ h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
+ }
+ }
+
+ render(h(Comp), nodeOps.createElement('div'))
+ expect(onTrack).toHaveBeenCalledTimes(3)
+ expect(events).toMatchObject([
+ {
+ target: obj,
+ type: OperationTypes.GET,
+ key: 'foo'
+ },
+ {
+ target: obj,
+ type: OperationTypes.HAS,
+ key: 'bar'
+ },
+ {
+ target: obj,
+ type: OperationTypes.ITERATE,
+ key: ITERATE_KEY
+ }
+ ])
+ })
+
+ it('onRenderTriggered', async () => {
+ const events: DebuggerEvent[] = []
+ const onTrigger = jest.fn((e: DebuggerEvent) => {
+ events.push(e)
+ })
+ const obj = reactive({ foo: 1, bar: 2 })
+
+ const Comp = {
+ setup() {
+ onRenderTriggered(onTrigger)
+ return () =>
+ h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
+ }
+ }
+
+ render(h(Comp), nodeOps.createElement('div'))
+
+ obj.foo++
+ await nextTick()
+ expect(onTrigger).toHaveBeenCalledTimes(1)
+ expect(events[0]).toMatchObject({
+ type: OperationTypes.SET,
+ key: 'foo',
+ oldValue: 1,
+ newValue: 2
+ })
+
+ delete obj.bar
+ await nextTick()
+ expect(onTrigger).toHaveBeenCalledTimes(2)
+ expect(events[1]).toMatchObject({
+ type: OperationTypes.DELETE,
+ key: 'bar',
+ oldValue: 2
+ })
+ ;(obj as any).baz = 3
+ await nextTick()
+ expect(onTrigger).toHaveBeenCalledTimes(3)
+ expect(events[2]).toMatchObject({
+ type: OperationTypes.ADD,
+ key: 'baz',
+ newValue: 3
+ })
+ })
+
+ test.todo('onErrorCaptured')
})
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index d8854f0a..9af70349 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -314,7 +314,7 @@ describe('api: watch', () => {
})
it('onTrack', async () => {
- let events: DebuggerEvent[] = []
+ const events: DebuggerEvent[] = []
let dummy
const onTrack = jest.fn((e: DebuggerEvent) => {
events.push(e)
@@ -331,14 +331,17 @@ describe('api: watch', () => {
expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toMatchObject([
{
+ target: obj,
type: OperationTypes.GET,
key: 'foo'
},
{
+ target: obj,
type: OperationTypes.HAS,
key: 'bar'
},
{
+ target: obj,
type: OperationTypes.ITERATE,
key: ITERATE_KEY
}
@@ -346,7 +349,7 @@ describe('api: watch', () => {
})
it('onTrigger', async () => {
- let events: DebuggerEvent[] = []
+ const events: DebuggerEvent[] = []
let dummy
const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e)
diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts
index ff8fdb84..c5976015 100644
--- a/packages/runtime-core/src/apiLifecycle.ts
+++ b/packages/runtime-core/src/apiLifecycle.ts
@@ -2,7 +2,7 @@ import { ComponentInstance, LifecycleHooks, currentInstance } from './component'
function injectHook(
name: keyof LifecycleHooks,
- hook: () => void,
+ hook: Function,
target: ComponentInstance | null | void = currentInstance
) {
if (target) {
@@ -14,41 +14,38 @@ function injectHook(
}
}
-export function onBeforeMount(hook: () => void, target?: ComponentInstance) {
+export function onBeforeMount(hook: Function, target?: ComponentInstance) {
injectHook('bm', hook, target)
}
-export function onMounted(hook: () => void, target?: ComponentInstance) {
+export function onMounted(hook: Function, target?: ComponentInstance) {
injectHook('m', hook, target)
}
-export function onBeforeUpdate(hook: () => void, target?: ComponentInstance) {
+export function onBeforeUpdate(hook: Function, target?: ComponentInstance) {
injectHook('bu', hook, target)
}
-export function onUpdated(hook: () => void, target?: ComponentInstance) {
+export function onUpdated(hook: Function, target?: ComponentInstance) {
injectHook('u', hook, target)
}
-export function onBeforeUnmount(hook: () => void, target?: ComponentInstance) {
+export function onBeforeUnmount(hook: Function, target?: ComponentInstance) {
injectHook('bum', hook, target)
}
-export function onUnmounted(hook: () => void, target?: ComponentInstance) {
+export function onUnmounted(hook: Function, target?: ComponentInstance) {
injectHook('um', hook, target)
}
-export function onRenderTriggered(
- hook: () => void,
- target?: ComponentInstance
-) {
+export function onRenderTriggered(hook: Function, target?: ComponentInstance) {
injectHook('rtg', hook, target)
}
-export function onRenderTracked(hook: () => void, target?: ComponentInstance) {
+export function onRenderTracked(hook: Function, target?: ComponentInstance) {
injectHook('rtc', hook, target)
}
-export function onErrorCaptured(hook: () => void, target?: ComponentInstance) {
+export function onErrorCaptured(hook: Function, target?: ComponentInstance) {
injectHook('ec', hook, target)
}
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 21e5b443..8b27fe86 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -162,14 +162,14 @@ export function createComponent(options: any) {
}
export function createComponentInstance(
- type: any,
+ vnode: VNode,
parent: ComponentInstance | null
): ComponentInstance {
const instance = {
- type,
+ vnode,
parent,
+ type: vnode.type as any,
root: null as any, // set later so it can point to itself
- vnode: null as any,
next: null,
subTree: null as any,
update: null as any,
diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts
index 2dd6c6ab..0552a838 100644
--- a/packages/runtime-core/src/createRenderer.ts
+++ b/packages/runtime-core/src/createRenderer.ts
@@ -565,21 +565,25 @@ export function createRenderer(options: RendererOptions) {
parentComponent: ComponentInstance | null,
isSVG: boolean
) {
- const Component = initialVNode.type as any
const instance: ComponentInstance = (initialVNode.component = createComponentInstance(
- Component,
+ initialVNode,
parentComponent
))
+
+ // resolve props and slots for setup context
+ const propsOptions = (initialVNode.type as any).props
+ resolveProps(instance, initialVNode.props, propsOptions)
+ resolveSlots(instance, initialVNode.children)
+
+ // setup stateful logic
+ if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
+ setupStatefulComponent(instance)
+ }
+
+ // create reactive effect for rendering
+ let mounted = false
instance.update = effect(function componentEffect() {
- if (instance.vnode === null) {
- // mountComponent
- instance.vnode = initialVNode
- resolveProps(instance, initialVNode.props, Component.props)
- resolveSlots(instance, initialVNode.children)
- // setup stateful
- if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
- setupStatefulComponent(instance)
- }
+ if (!mounted) {
const subTree = (instance.subTree = renderComponentRoot(instance))
// beforeMount hook
if (instance.bm !== null) {
@@ -591,6 +595,7 @@ export function createRenderer(options: RendererOptions) {
if (instance.m !== null) {
queuePostFlushCb(instance.m)
}
+ mounted = true
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
@@ -601,7 +606,7 @@ export function createRenderer(options: RendererOptions) {
next.component = instance
instance.vnode = next
instance.next = null
- resolveProps(instance, next.props, Component.props)
+ resolveProps(instance, next.props, propsOptions)
resolveSlots(instance, next.children)
}
const prevTree = instance.subTree