mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): lifecycle beforeUpdate and updated hooks (#89)
This commit is contained in:
parent
bb8cc447eb
commit
2cce436aaf
|
@ -175,4 +175,82 @@ describe('baseWatch', () => {
|
||||||
scope.stop()
|
scope.stop()
|
||||||
expect(calls).toEqual(['sync 2', 'post 2'])
|
expect(calls).toEqual(['sync 2', 'post 2'])
|
||||||
})
|
})
|
||||||
|
test('baseWatch with middleware', async () => {
|
||||||
|
let effectCalls: string[] = []
|
||||||
|
let watchCalls: string[] = []
|
||||||
|
const source = ref(0)
|
||||||
|
|
||||||
|
// effect
|
||||||
|
baseWatch(
|
||||||
|
() => {
|
||||||
|
source.value
|
||||||
|
effectCalls.push('effect')
|
||||||
|
onEffectCleanup(() => effectCalls.push('effect cleanup'))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
scheduler,
|
||||||
|
middleware: next => {
|
||||||
|
effectCalls.push('before effect running')
|
||||||
|
next()
|
||||||
|
effectCalls.push('effect ran')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// watch
|
||||||
|
baseWatch(
|
||||||
|
() => source.value,
|
||||||
|
() => {
|
||||||
|
watchCalls.push('watch')
|
||||||
|
onEffectCleanup(() => watchCalls.push('watch cleanup'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scheduler,
|
||||||
|
middleware: next => {
|
||||||
|
watchCalls.push('before watch running')
|
||||||
|
next()
|
||||||
|
watchCalls.push('watch ran')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(effectCalls).toEqual([])
|
||||||
|
expect(watchCalls).toEqual([])
|
||||||
|
await nextTick()
|
||||||
|
expect(effectCalls).toEqual([
|
||||||
|
'before effect running',
|
||||||
|
'effect',
|
||||||
|
'effect ran',
|
||||||
|
])
|
||||||
|
expect(watchCalls).toEqual([])
|
||||||
|
effectCalls.length = 0
|
||||||
|
watchCalls.length = 0
|
||||||
|
|
||||||
|
source.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(effectCalls).toEqual([
|
||||||
|
'before effect running',
|
||||||
|
'effect cleanup',
|
||||||
|
'effect',
|
||||||
|
'effect ran',
|
||||||
|
])
|
||||||
|
expect(watchCalls).toEqual(['before watch running', 'watch', 'watch ran'])
|
||||||
|
effectCalls.length = 0
|
||||||
|
watchCalls.length = 0
|
||||||
|
|
||||||
|
source.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(effectCalls).toEqual([
|
||||||
|
'before effect running',
|
||||||
|
'effect cleanup',
|
||||||
|
'effect',
|
||||||
|
'effect ran',
|
||||||
|
])
|
||||||
|
expect(watchCalls).toEqual([
|
||||||
|
'before watch running',
|
||||||
|
'watch cleanup',
|
||||||
|
'watch',
|
||||||
|
'watch ran',
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -71,6 +71,7 @@ export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
|
||||||
deep?: boolean
|
deep?: boolean
|
||||||
once?: boolean
|
once?: boolean
|
||||||
scheduler?: Scheduler
|
scheduler?: Scheduler
|
||||||
|
middleware?: BaseWatchMiddleware
|
||||||
onError?: HandleError
|
onError?: HandleError
|
||||||
onWarn?: HandleWarn
|
onWarn?: HandleWarn
|
||||||
}
|
}
|
||||||
|
@ -83,6 +84,7 @@ export type Scheduler = (
|
||||||
effect: ReactiveEffect,
|
effect: ReactiveEffect,
|
||||||
isInit: boolean,
|
isInit: boolean,
|
||||||
) => void
|
) => void
|
||||||
|
export type BaseWatchMiddleware = (next: () => unknown) => any
|
||||||
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
|
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
|
||||||
export type HandleWarn = (msg: string, ...args: any[]) => void
|
export type HandleWarn = (msg: string, ...args: any[]) => void
|
||||||
|
|
||||||
|
@ -132,6 +134,7 @@ export function baseWatch(
|
||||||
scheduler = DEFAULT_SCHEDULER,
|
scheduler = DEFAULT_SCHEDULER,
|
||||||
onWarn = __DEV__ ? warn : NOOP,
|
onWarn = __DEV__ ? warn : NOOP,
|
||||||
onError = DEFAULT_HANDLE_ERROR,
|
onError = DEFAULT_HANDLE_ERROR,
|
||||||
|
middleware,
|
||||||
onTrack,
|
onTrack,
|
||||||
onTrigger,
|
onTrigger,
|
||||||
}: BaseWatchOptions = EMPTY_OBJ,
|
}: BaseWatchOptions = EMPTY_OBJ,
|
||||||
|
@ -211,6 +214,10 @@ export function baseWatch(
|
||||||
activeEffect = currentEffect
|
activeEffect = currentEffect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (middleware) {
|
||||||
|
const baseGetter = getter
|
||||||
|
getter = () => middleware(baseGetter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getter = NOOP
|
getter = NOOP
|
||||||
|
@ -264,6 +271,7 @@ export function baseWatch(
|
||||||
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
|
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
|
||||||
: hasChanged(newValue, oldValue))
|
: hasChanged(newValue, oldValue))
|
||||||
) {
|
) {
|
||||||
|
const next = () => {
|
||||||
// cleanup before running cb again
|
// cleanup before running cb again
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
@ -272,7 +280,7 @@ export function baseWatch(
|
||||||
activeEffect = effect
|
activeEffect = effect
|
||||||
try {
|
try {
|
||||||
callWithAsyncErrorHandling(
|
callWithAsyncErrorHandling(
|
||||||
cb,
|
cb!,
|
||||||
onError,
|
onError,
|
||||||
BaseWatchErrorCodes.WATCH_CALLBACK,
|
BaseWatchErrorCodes.WATCH_CALLBACK,
|
||||||
[
|
[
|
||||||
|
@ -291,6 +299,12 @@ export function baseWatch(
|
||||||
activeEffect = currentEffect
|
activeEffect = currentEffect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (middleware) {
|
||||||
|
middleware(next)
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// watchEffect
|
// watchEffect
|
||||||
effect.run()
|
effect.run()
|
||||||
|
|
|
@ -76,5 +76,6 @@ export {
|
||||||
traverse,
|
traverse,
|
||||||
BaseWatchErrorCodes,
|
BaseWatchErrorCodes,
|
||||||
type BaseWatchOptions,
|
type BaseWatchOptions,
|
||||||
|
type BaseWatchMiddleware,
|
||||||
type Scheduler,
|
type Scheduler,
|
||||||
} from './baseWatch'
|
} from './baseWatch'
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import {
|
import {
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onBeforeUpdate,
|
||||||
onEffectCleanup,
|
onEffectCleanup,
|
||||||
|
onUpdated,
|
||||||
ref,
|
ref,
|
||||||
render,
|
render,
|
||||||
renderEffect,
|
renderEffect,
|
||||||
|
@ -25,6 +27,27 @@ beforeEach(() => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
host.remove()
|
host.remove()
|
||||||
})
|
})
|
||||||
|
const createDemo = (
|
||||||
|
setupFn: (porps: any, ctx: any) => any,
|
||||||
|
renderFn: (ctx: any) => any,
|
||||||
|
) => {
|
||||||
|
const demo = defineComponent({
|
||||||
|
setup(...args) {
|
||||||
|
const returned = setupFn(...args)
|
||||||
|
Object.defineProperty(returned, '__isScriptSetup', {
|
||||||
|
enumerable: false,
|
||||||
|
value: true,
|
||||||
|
})
|
||||||
|
return returned
|
||||||
|
},
|
||||||
|
})
|
||||||
|
demo.render = (ctx: any) => {
|
||||||
|
const t0 = template('<div></div>')
|
||||||
|
renderFn(ctx)
|
||||||
|
return t0()
|
||||||
|
}
|
||||||
|
return () => render(demo as any, {}, '#host')
|
||||||
|
}
|
||||||
|
|
||||||
describe('renderWatch', () => {
|
describe('renderWatch', () => {
|
||||||
test('effect', async () => {
|
test('effect', async () => {
|
||||||
|
@ -53,16 +76,26 @@ describe('renderWatch', () => {
|
||||||
expect(dummy).toBe(1)
|
expect(dummy).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('scheduling order', async () => {
|
test('should run with the scheduling order', async () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
|
|
||||||
const demo = defineComponent({
|
const mount = createDemo(
|
||||||
setup() {
|
() => {
|
||||||
|
// setup
|
||||||
const source = ref(0)
|
const source = ref(0)
|
||||||
const renderSource = ref(0)
|
const renderSource = ref(0)
|
||||||
const change = () => source.value++
|
const change = () => source.value++
|
||||||
const changeRender = () => renderSource.value++
|
const changeRender = () => renderSource.value++
|
||||||
|
|
||||||
|
// Life Cycle Hooks
|
||||||
|
onUpdated(() => {
|
||||||
|
calls.push(`updated ${source.value}`)
|
||||||
|
})
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
calls.push(`beforeUpdate ${source.value}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch API
|
||||||
watchPostEffect(() => {
|
watchPostEffect(() => {
|
||||||
const current = source.value
|
const current = source.value
|
||||||
calls.push(`post ${current}`)
|
calls.push(`post ${current}`)
|
||||||
|
@ -78,17 +111,11 @@ describe('renderWatch', () => {
|
||||||
calls.push(`sync ${current}`)
|
calls.push(`sync ${current}`)
|
||||||
onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
|
onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
|
||||||
})
|
})
|
||||||
const __returned__ = { source, change, renderSource, changeRender }
|
return { source, change, renderSource, changeRender }
|
||||||
Object.defineProperty(__returned__, '__isScriptSetup', {
|
|
||||||
enumerable: false,
|
|
||||||
value: true,
|
|
||||||
})
|
|
||||||
return __returned__
|
|
||||||
},
|
},
|
||||||
})
|
// render
|
||||||
|
(_ctx) => {
|
||||||
demo.render = (_ctx: any) => {
|
// Render Watch API
|
||||||
const t0 = template('<div></div>')
|
|
||||||
renderEffect(() => {
|
renderEffect(() => {
|
||||||
const current = _ctx.renderSource
|
const current = _ctx.renderSource
|
||||||
calls.push(`renderEffect ${current}`)
|
calls.push(`renderEffect ${current}`)
|
||||||
|
@ -101,10 +128,11 @@ describe('renderWatch', () => {
|
||||||
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
|
onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return t0()
|
},
|
||||||
}
|
)
|
||||||
|
|
||||||
const instance = render(demo as any, {}, '#host')
|
// Mount
|
||||||
|
const instance = mount()
|
||||||
const { change, changeRender } = instance.setupState as any
|
const { change, changeRender } = instance.setupState as any
|
||||||
|
|
||||||
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0'])
|
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0'])
|
||||||
|
@ -114,8 +142,10 @@ describe('renderWatch', () => {
|
||||||
expect(calls).toEqual(['post 0'])
|
expect(calls).toEqual(['post 0'])
|
||||||
calls.length = 0
|
calls.length = 0
|
||||||
|
|
||||||
|
// Update
|
||||||
changeRender()
|
changeRender()
|
||||||
change()
|
change()
|
||||||
|
|
||||||
expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
|
expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
|
||||||
calls.length = 0
|
calls.length = 0
|
||||||
|
|
||||||
|
@ -123,11 +153,75 @@ describe('renderWatch', () => {
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
'pre cleanup 0',
|
'pre cleanup 0',
|
||||||
'pre 1',
|
'pre 1',
|
||||||
|
'beforeUpdate 1',
|
||||||
'renderEffect cleanup 0',
|
'renderEffect cleanup 0',
|
||||||
'renderEffect 1',
|
'renderEffect 1',
|
||||||
'renderWatch 1',
|
'renderWatch 1',
|
||||||
'post cleanup 0',
|
'post cleanup 0',
|
||||||
'post 1',
|
'post 1',
|
||||||
|
'updated 1',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('errors should include the execution location with beforeUpdate hook', async () => {
|
||||||
|
const mount = createDemo(
|
||||||
|
// setup
|
||||||
|
() => {
|
||||||
|
const source = ref()
|
||||||
|
const update = () => source.value++
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
throw 'error in beforeUpdate'
|
||||||
|
})
|
||||||
|
return { source, update }
|
||||||
|
},
|
||||||
|
// render
|
||||||
|
(ctx) => {
|
||||||
|
renderEffect(() => {
|
||||||
|
ctx.source
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const instance = mount()
|
||||||
|
const { update } = instance.setupState as any
|
||||||
|
await expect(async () => {
|
||||||
|
update()
|
||||||
|
await nextTick()
|
||||||
|
}).rejects.toThrow('error in beforeUpdate')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
'[Vue warn] Unhandled error during execution of beforeUpdate hook',
|
||||||
|
).toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('errors should include the execution location with updated hook', async () => {
|
||||||
|
const mount = createDemo(
|
||||||
|
// setup
|
||||||
|
() => {
|
||||||
|
const source = ref(0)
|
||||||
|
const update = () => source.value++
|
||||||
|
onUpdated(() => {
|
||||||
|
throw 'error in updated'
|
||||||
|
})
|
||||||
|
return { source, update }
|
||||||
|
},
|
||||||
|
// render
|
||||||
|
(ctx) => {
|
||||||
|
renderEffect(() => {
|
||||||
|
ctx.source
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const instance = mount()
|
||||||
|
const { update } = instance.setupState as any
|
||||||
|
await expect(async () => {
|
||||||
|
update()
|
||||||
|
await nextTick()
|
||||||
|
}).rejects.toThrow('error in updated')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
'[Vue warn] Unhandled error during execution of updated',
|
||||||
|
).toHaveBeenWarned()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { EffectScope, type Ref, ref } from '@vue/reactivity'
|
import {
|
||||||
|
EffectScope,
|
||||||
|
type Ref,
|
||||||
|
pauseTracking,
|
||||||
|
ref,
|
||||||
|
resetTracking,
|
||||||
|
} from '@vue/reactivity'
|
||||||
|
|
||||||
import { EMPTY_OBJ } from '@vue/shared'
|
import { EMPTY_OBJ } from '@vue/shared'
|
||||||
import type { Block } from './render'
|
import type { Block } from './render'
|
||||||
|
@ -47,6 +53,7 @@ export interface ComponentInternalInstance {
|
||||||
// lifecycle
|
// lifecycle
|
||||||
get isMounted(): boolean
|
get isMounted(): boolean
|
||||||
get isUnmounted(): boolean
|
get isUnmounted(): boolean
|
||||||
|
isUpdating: boolean
|
||||||
isUnmountedRef: Ref<boolean>
|
isUnmountedRef: Ref<boolean>
|
||||||
isMountedRef: Ref<boolean>
|
isMountedRef: Ref<boolean>
|
||||||
// TODO: registory of provides, lifecycles, ...
|
// TODO: registory of provides, lifecycles, ...
|
||||||
|
@ -150,11 +157,18 @@ export const createComponentInstance = (
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
get isMounted() {
|
get isMounted() {
|
||||||
return isMountedRef.value
|
pauseTracking()
|
||||||
|
const value = isMountedRef.value
|
||||||
|
resetTracking()
|
||||||
|
return value
|
||||||
},
|
},
|
||||||
get isUnmounted() {
|
get isUnmounted() {
|
||||||
return isUnmountedRef.value
|
pauseTracking()
|
||||||
|
const value = isUnmountedRef.value
|
||||||
|
resetTracking()
|
||||||
|
return value
|
||||||
},
|
},
|
||||||
|
isUpdating: false,
|
||||||
isMountedRef,
|
isMountedRef,
|
||||||
isUnmountedRef,
|
isUnmountedRef,
|
||||||
// TODO: registory of provides, appContext, lifecycles, ...
|
// TODO: registory of provides, appContext, lifecycles, ...
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { isFunction } from '@vue/shared'
|
import { NOOP, isFunction } from '@vue/shared'
|
||||||
import { type ComponentInternalInstance, currentInstance } from './component'
|
import { type ComponentInternalInstance, currentInstance } from './component'
|
||||||
import { watchEffect } from './apiWatch'
|
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
||||||
|
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
||||||
|
import { renderWatch } from './renderWatch'
|
||||||
|
|
||||||
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
||||||
|
|
||||||
|
@ -27,7 +29,7 @@ export type DirectiveHookName =
|
||||||
| 'created'
|
| 'created'
|
||||||
| 'beforeMount'
|
| 'beforeMount'
|
||||||
| 'mounted'
|
| 'mounted'
|
||||||
// | 'beforeUpdate'
|
| 'beforeUpdate'
|
||||||
| 'updated'
|
| 'updated'
|
||||||
| 'beforeUnmount'
|
| 'beforeUnmount'
|
||||||
| 'unmounted'
|
| 'unmounted'
|
||||||
|
@ -93,12 +95,12 @@ export function withDirectives<T extends Node>(
|
||||||
}
|
}
|
||||||
bindings.push(binding)
|
bindings.push(binding)
|
||||||
|
|
||||||
callDirectiveHook(node, binding, 'created')
|
callDirectiveHook(node, binding, instance, 'created')
|
||||||
|
|
||||||
watchEffect(() => {
|
// register source
|
||||||
if (!instance.isMountedRef.value) return
|
if (source) {
|
||||||
callDirectiveHook(node, binding, 'updated')
|
renderWatch(source, NOOP)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
@ -114,7 +116,7 @@ export function invokeDirectiveHook(
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const directives = instance.dirs.get(node) || []
|
const directives = instance.dirs.get(node) || []
|
||||||
for (const binding of directives) {
|
for (const binding of directives) {
|
||||||
callDirectiveHook(node, binding, name)
|
callDirectiveHook(node, binding, instance, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,6 +124,7 @@ export function invokeDirectiveHook(
|
||||||
function callDirectiveHook(
|
function callDirectiveHook(
|
||||||
node: Node,
|
node: Node,
|
||||||
binding: DirectiveBinding,
|
binding: DirectiveBinding,
|
||||||
|
instance: ComponentInternalInstance | null,
|
||||||
name: DirectiveHookName,
|
name: DirectiveHookName,
|
||||||
) {
|
) {
|
||||||
const { dir } = binding
|
const { dir } = binding
|
||||||
|
@ -129,9 +132,14 @@ function callDirectiveHook(
|
||||||
if (!hook) return
|
if (!hook) return
|
||||||
|
|
||||||
const newValue = binding.source ? binding.source() : undefined
|
const newValue = binding.source ? binding.source() : undefined
|
||||||
if (name === 'updated' && binding.value === newValue) return
|
|
||||||
|
|
||||||
binding.oldValue = binding.value
|
|
||||||
binding.value = newValue
|
binding.value = newValue
|
||||||
hook(node, binding)
|
// disable tracking inside all lifecycle hooks
|
||||||
|
// since they can potentially be called inside effects.
|
||||||
|
pauseTracking()
|
||||||
|
callWithAsyncErrorHandling(hook, instance, VaporErrorCodes.DIRECTIVE_HOOK, [
|
||||||
|
node,
|
||||||
|
binding,
|
||||||
|
])
|
||||||
|
resetTracking()
|
||||||
|
if (name !== 'beforeUpdate') binding.oldValue = binding.value
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import {
|
import {
|
||||||
type BaseWatchErrorCodes,
|
type BaseWatchErrorCodes,
|
||||||
|
type BaseWatchMiddleware,
|
||||||
type BaseWatchOptions,
|
type BaseWatchOptions,
|
||||||
baseWatch,
|
baseWatch,
|
||||||
getCurrentScope,
|
getCurrentScope,
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { NOOP, remove } from '@vue/shared'
|
import { NOOP, invokeArrayFns, remove } from '@vue/shared'
|
||||||
import { currentInstance } from './component'
|
import { type ComponentInternalInstance, currentInstance } from './component'
|
||||||
import { createVaporRenderingScheduler } from './scheduler'
|
import {
|
||||||
|
createVaporRenderingScheduler,
|
||||||
|
queuePostRenderEffect,
|
||||||
|
} from './scheduler'
|
||||||
import { handleError as handleErrorWithInstance } from './errorHandling'
|
import { handleError as handleErrorWithInstance } from './errorHandling'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
|
import { invokeDirectiveHook } from './directive'
|
||||||
|
|
||||||
type WatchStopHandle = () => void
|
type WatchStopHandle = () => void
|
||||||
|
|
||||||
|
@ -28,8 +33,6 @@ function doWatch(source: any, cb?: any): WatchStopHandle {
|
||||||
|
|
||||||
if (__DEV__) extendOptions.onWarn = warn
|
if (__DEV__) extendOptions.onWarn = warn
|
||||||
|
|
||||||
// TODO: Life Cycle Hooks
|
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (__SSR__) {}
|
// if (__SSR__) {}
|
||||||
|
|
||||||
|
@ -40,6 +43,8 @@ function doWatch(source: any, cb?: any): WatchStopHandle {
|
||||||
handleErrorWithInstance(err, instance, type)
|
handleErrorWithInstance(err, instance, type)
|
||||||
extendOptions.scheduler = createVaporRenderingScheduler(instance)
|
extendOptions.scheduler = createVaporRenderingScheduler(instance)
|
||||||
|
|
||||||
|
extendOptions.middleware = createMiddleware(instance)
|
||||||
|
|
||||||
let effect = baseWatch(source, cb, extendOptions)
|
let effect = baseWatch(source, cb, extendOptions)
|
||||||
|
|
||||||
const unwatch = !effect
|
const unwatch = !effect
|
||||||
|
@ -53,3 +58,44 @@ function doWatch(source: any, cb?: any): WatchStopHandle {
|
||||||
|
|
||||||
return unwatch
|
return unwatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createMiddleware =
|
||||||
|
(instance: ComponentInternalInstance | null): BaseWatchMiddleware =>
|
||||||
|
(next) => {
|
||||||
|
let value: unknown
|
||||||
|
// with lifecycle
|
||||||
|
if (instance && instance.isMounted) {
|
||||||
|
const { bu, u, dirs } = instance
|
||||||
|
// beforeUpdate hook
|
||||||
|
const isFirstEffect = !instance.isUpdating
|
||||||
|
if (isFirstEffect) {
|
||||||
|
if (bu) {
|
||||||
|
invokeArrayFns(bu)
|
||||||
|
}
|
||||||
|
if (dirs) {
|
||||||
|
invokeDirectiveHook(instance, 'beforeUpdate')
|
||||||
|
}
|
||||||
|
instance.isUpdating = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// run callback
|
||||||
|
value = next()
|
||||||
|
|
||||||
|
if (isFirstEffect) {
|
||||||
|
queuePostRenderEffect(() => {
|
||||||
|
instance.isUpdating = false
|
||||||
|
if (dirs) {
|
||||||
|
invokeDirectiveHook(instance, 'updated')
|
||||||
|
}
|
||||||
|
// updated hook
|
||||||
|
if (u) {
|
||||||
|
queuePostRenderEffect(u)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// is not mounted
|
||||||
|
value = next()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Scheduler } from '@vue/reactivity'
|
import type { Scheduler } from '@vue/reactivity'
|
||||||
import type { ComponentInternalInstance } from './component'
|
import type { ComponentInternalInstance } from './component'
|
||||||
|
import { isArray } from '@vue/shared'
|
||||||
|
|
||||||
export interface SchedulerJob extends Function {
|
export interface SchedulerJob extends Function {
|
||||||
id?: number
|
id?: number
|
||||||
|
@ -73,7 +74,8 @@ function queueJob(job: SchedulerJob) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queuePostRenderEffect(cb: SchedulerJob) {
|
export function queuePostRenderEffect(cb: SchedulerJobs) {
|
||||||
|
if (!isArray(cb)) {
|
||||||
if (
|
if (
|
||||||
!activePostFlushCbs ||
|
!activePostFlushCbs ||
|
||||||
!activePostFlushCbs.includes(
|
!activePostFlushCbs.includes(
|
||||||
|
@ -83,6 +85,12 @@ export function queuePostRenderEffect(cb: SchedulerJob) {
|
||||||
) {
|
) {
|
||||||
pendingPostFlushCbs.push(cb)
|
pendingPostFlushCbs.push(cb)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// if cb is an array, it is a component lifecycle hook which can only be
|
||||||
|
// triggered by a job, which is already deduped in the main queue, so
|
||||||
|
// we can skip duplicate check here to improve perf
|
||||||
|
pendingPostFlushCbs.push(...cb)
|
||||||
|
}
|
||||||
queueFlush()
|
queueFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
onMounted,
|
onMounted,
|
||||||
onBeforeMount,
|
onBeforeMount,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
onBeforeUpdate,
|
||||||
|
onUpdated,
|
||||||
} from 'vue/vapor'
|
} from 'vue/vapor'
|
||||||
|
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
|
@ -26,12 +28,24 @@ onMounted(() => {
|
||||||
count.value++
|
count.value++
|
||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
console.log('before updated')
|
||||||
|
})
|
||||||
|
onUpdated(() => {
|
||||||
|
console.log('updated')
|
||||||
|
})
|
||||||
|
|
||||||
|
const log = (arg: any) => {
|
||||||
|
console.log('callback in render effect')
|
||||||
|
return arg
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="red">Counter</h1>
|
<h1 class="red">Counter</h1>
|
||||||
<div>The number is {{ count }}.</div>
|
<div>The number is {{ log(count) }}.</div>
|
||||||
<div>{{ count }} * 2 = {{ double }}</div>
|
<div>{{ count }} * 2 = {{ double }}</div>
|
||||||
<div style="display: flex; gap: 8px">
|
<div style="display: flex; gap: 8px">
|
||||||
<button @click="inc">inc</button>
|
<button @click="inc">inc</button>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { ObjectDirective, FunctionDirective, ref } from '@vue/vapor'
|
import { ObjectDirective, FunctionDirective, ref } from '@vue/vapor'
|
||||||
|
|
||||||
const text = ref('created (overwrite by v-text), ')
|
const text = ref('created (overwrite by v-text), ')
|
||||||
|
const counter = ref(0)
|
||||||
const vDirective: ObjectDirective<HTMLDivElement, undefined> = {
|
const vDirective: ObjectDirective<HTMLDivElement, undefined> = {
|
||||||
created(node) {
|
created(node) {
|
||||||
if (!node.parentElement) {
|
if (!node.parentElement) {
|
||||||
|
@ -17,9 +18,15 @@ const vDirective: ObjectDirective<HTMLDivElement, undefined> = {
|
||||||
mounted(node) {
|
mounted(node) {
|
||||||
if (node.parentElement) node.textContent += 'mounted, '
|
if (node.parentElement) node.textContent += 'mounted, '
|
||||||
},
|
},
|
||||||
|
beforeUpdate(node, binding) {
|
||||||
|
console.log('beforeUpdate', binding, node)
|
||||||
|
},
|
||||||
|
updated(node, binding) {
|
||||||
|
console.log('updated', binding, node)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
const vDirectiveSimple: FunctionDirective<HTMLDivElement> = (node, binding) => {
|
const vDirectiveSimple: FunctionDirective<HTMLDivElement> = (node, binding) => {
|
||||||
console.log(node, binding)
|
console.log('v-directive-simple:', node, binding)
|
||||||
}
|
}
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
text.value = 'change'
|
text.value = 'change'
|
||||||
|
@ -33,4 +40,15 @@ const handleClick = () => {
|
||||||
v-directive-simple="text"
|
v-directive-simple="text"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
/>
|
/>
|
||||||
|
<button @click="counter++">
|
||||||
|
{{ counter }} (Click to Update Other Element)
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
color-scheme: dark;
|
||||||
|
background-color: #000;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue