diff --git a/packages/runtime-vapor/__tests__/renderWatch.spec.ts b/packages/runtime-vapor/__tests__/renderEffect.spec.ts similarity index 81% rename from packages/runtime-vapor/__tests__/renderWatch.spec.ts rename to packages/runtime-vapor/__tests__/renderEffect.spec.ts index 88c156311..cdf974e53 100644 --- a/packages/runtime-vapor/__tests__/renderWatch.spec.ts +++ b/packages/runtime-vapor/__tests__/renderEffect.spec.ts @@ -1,11 +1,10 @@ +import { onEffectCleanup } from '@vue/reactivity' import { nextTick, onBeforeUpdate, onUpdated, - onWatcherCleanup, ref, renderEffect, - renderWatch, template, watchEffect, watchPostEffect, @@ -31,8 +30,8 @@ const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) => }, }) -describe('renderWatch', () => { - test('effect', async () => { +describe('renderEffect', () => { + test('basic', async () => { let dummy: any const source = ref(0) renderEffect(() => { @@ -58,26 +57,6 @@ describe('renderWatch', () => { expect(dummy).toBe(3) }) - test('watch', async () => { - let dummy: any - const source = ref(0) - renderWatch(source, () => { - dummy = source.value - }) - await nextTick() - expect(dummy).toBe(undefined) - - source.value++ - expect(dummy).toBe(undefined) - await nextTick() - expect(dummy).toBe(1) - - source.value++ - expect(dummy).toBe(1) - await nextTick() - expect(dummy).toBe(2) - }) - test('should run with the scheduling order', async () => { const calls: string[] = [] @@ -101,17 +80,17 @@ describe('renderWatch', () => { watchPostEffect(() => { const current = source.value calls.push(`post ${current}`) - onWatcherCleanup(() => calls.push(`post cleanup ${current}`)) + onEffectCleanup(() => calls.push(`post cleanup ${current}`)) }) watchEffect(() => { const current = source.value calls.push(`pre ${current}`) - onWatcherCleanup(() => calls.push(`pre cleanup ${current}`)) + onEffectCleanup(() => calls.push(`pre cleanup ${current}`)) }) watchSyncEffect(() => { const current = source.value calls.push(`sync ${current}`) - onWatcherCleanup(() => calls.push(`sync cleanup ${current}`)) + onEffectCleanup(() => calls.push(`sync cleanup ${current}`)) }) return { source, change, renderSource, changeRender } }, @@ -121,15 +100,8 @@ describe('renderWatch', () => { renderEffect(() => { const current = _ctx.renderSource calls.push(`renderEffect ${current}`) - onWatcherCleanup(() => calls.push(`renderEffect cleanup ${current}`)) + onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`)) }) - renderWatch( - () => _ctx.renderSource, - value => { - calls.push(`renderWatch ${value}`) - onWatcherCleanup(() => calls.push(`renderWatch cleanup ${value}`)) - }, - ) }, ).render() const { change, changeRender } = instance.setupState as any @@ -151,7 +123,6 @@ describe('renderWatch', () => { 'beforeUpdate 1', 'renderEffect cleanup 0', 'renderEffect 1', - 'renderWatch 1', 'post cleanup 0', 'post 1', 'updated 1', @@ -172,8 +143,6 @@ describe('renderWatch', () => { 'beforeUpdate 2', 'renderEffect cleanup 1', 'renderEffect 2', - 'renderWatch cleanup 1', - 'renderWatch 2', 'post cleanup 1', 'post 2', 'updated 2', diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 1d31aa41b..cefcba1a8 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -1,7 +1,7 @@ import { type EffectScope, effectScope, isReactive } from '@vue/reactivity' import { isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode, insert, remove } from './dom/element' -import { renderEffect } from './renderWatch' +import { renderEffect } from './renderEffect' import { type Block, type Fragment, fragmentKey } from './apiRender' import { warn } from './warning' diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index 33270ef0c..967dc3b33 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -1,4 +1,4 @@ -import { renderWatch } from './renderWatch' +import { renderEffect } from './renderEffect' import { type Block, type Fragment, fragmentKey } from './apiRender' import { type EffectScope, effectScope } from '@vue/reactivity' import { createComment, createTextNode, insert, remove } from './dom/element' @@ -12,6 +12,8 @@ export const createIf = ( b2?: BlockFn, // hydrationNode?: Node, ): Fragment => { + let newValue: any + let oldValue: any let branch: BlockFn | undefined let parent: ParentNode | undefined | null let block: Block | undefined @@ -29,15 +31,14 @@ export const createIf = ( // setCurrentHydrationNode(hydrationNode!) // } - renderWatch( - () => !!condition(), - value => { + renderEffect(() => { + if ((newValue = !!condition()) !== oldValue) { parent ||= anchor.parentNode if (block) { scope!.stop() remove(block, parent!) } - if ((branch = value ? b1 : b2)) { + if ((branch = (oldValue = newValue) ? b1 : b2)) { scope = effectScope() fragment.nodes = block = scope.run(branch)! parent && insert(block, parent, anchor) @@ -45,9 +46,8 @@ export const createIf = ( scope = block = undefined fragment.nodes = [] } - }, - { immediate: true }, - ) + } + }) // TODO: SSR // if (isHydrating) { diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directives.ts index 15d2119c4..3eab22b2c 100644 --- a/packages/runtime-vapor/src/directives.ts +++ b/packages/runtime-vapor/src/directives.ts @@ -1,8 +1,8 @@ -import { NOOP, isFunction } from '@vue/shared' +import { isFunction } from '@vue/shared' import { type ComponentInternalInstance, currentInstance } from './component' -import { pauseTracking, resetTracking } from '@vue/reactivity' +import { pauseTracking, resetTracking, traverse } from '@vue/reactivity' import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling' -import { renderWatch } from './renderWatch' +import { renderEffect } from './renderEffect' export type DirectiveModifiers = Record @@ -100,8 +100,12 @@ export function withDirectives( // register source if (source) { - // callback will be overridden by middleware - renderWatch(source, NOOP, { deep: dir.deep }) + if (dir.deep) { + const deep = dir.deep === true ? undefined : dir.deep + const baseSource = source + source = () => traverse(baseSource(), deep) + } + renderEffect(source) } } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index b5687fc1a..03d79a3c7 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -46,7 +46,7 @@ export { type FunctionalComponent, type SetupFn, } from './component' -export { renderEffect, renderWatch } from './renderWatch' +export { renderEffect } from './renderEffect' export { watch, watchEffect, diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts new file mode 100644 index 000000000..b44153ace --- /dev/null +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -0,0 +1,59 @@ +import { EffectFlags, ReactiveEffect, type SchedulerJob } from '@vue/reactivity' +import { invokeArrayFns } from '@vue/shared' +import { getCurrentInstance, setCurrentInstance } from './component' +import { queueJob, queuePostRenderEffect } from './scheduler' +import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling' +import { invokeDirectiveHook } from './directives' + +export function renderEffect(cb: () => void) { + const instance = getCurrentInstance() + + let effect: ReactiveEffect + + const job: SchedulerJob = () => { + if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) { + return + } + + if (instance?.isMounted && !instance.isUpdating) { + instance.isUpdating = true + + const { bu, u, dirs } = instance + // beforeUpdate hook + if (bu) { + invokeArrayFns(bu) + } + if (dirs) { + invokeDirectiveHook(instance, 'beforeUpdate') + } + + effect.run() + + queuePostRenderEffect(() => { + instance.isUpdating = false + if (dirs) { + invokeDirectiveHook(instance, 'updated') + } + // updated hook + if (u) { + queuePostRenderEffect(u) + } + }) + } else { + effect.run() + } + } + + effect = new ReactiveEffect(() => { + const reset = instance && setCurrentInstance(instance) + callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION) + reset?.() + }) + + effect.scheduler = () => { + if (instance) job.id = instance.uid + queueJob(job) + } + + effect.run() +} diff --git a/packages/runtime-vapor/src/renderWatch.ts b/packages/runtime-vapor/src/renderWatch.ts deleted file mode 100644 index 71364037f..000000000 --- a/packages/runtime-vapor/src/renderWatch.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - type BaseWatchErrorCodes, - type BaseWatchMiddleware, - type BaseWatchOptions, - baseWatch, -} from '@vue/reactivity' -import { NOOP, extend, invokeArrayFns, remove } from '@vue/shared' -import { - type ComponentInternalInstance, - getCurrentInstance, - setCurrentInstance, -} from './component' -import { - createVaporRenderingScheduler, - queuePostRenderEffect, -} from './scheduler' -import { handleError as handleErrorWithInstance } from './errorHandling' -import { warn } from './warning' -import { invokeDirectiveHook } from './directives' - -interface RenderWatchOptions { - immediate?: boolean - deep?: boolean - once?: boolean -} - -type WatchStopHandle = () => void - -export function renderEffect(effect: () => void): WatchStopHandle { - return doWatch(effect) -} - -export function renderWatch( - source: any, - cb: (value: any, oldValue: any) => void, - options?: RenderWatchOptions, -): WatchStopHandle { - return doWatch(source as any, cb, options) -} - -function doWatch( - source: any, - cb?: any, - options?: RenderWatchOptions, -): WatchStopHandle { - const extendOptions: BaseWatchOptions = - cb && options ? extend({}, options) : {} - - if (__DEV__) extendOptions.onWarn = warn - - // TODO: SSR - // if (__SSR__) {} - - const instance = getCurrentInstance() - extend(extendOptions, { - onError: (err: unknown, type: BaseWatchErrorCodes) => - handleErrorWithInstance(err, instance, type), - scheduler: createVaporRenderingScheduler(instance), - middleware: createMiddleware(instance), - }) - let effect = baseWatch(source, cb, extendOptions) - - const unwatch = !effect - ? NOOP - : () => { - effect!.stop() - if (instance && instance.scope) { - remove(instance.scope.effects!, effect) - } - } - - 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 - } - - const reset = setCurrentInstance(instance) - // run callback - value = next() - reset() - - 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 - } diff --git a/packages/runtime-vapor/src/scheduler.ts b/packages/runtime-vapor/src/scheduler.ts index 4685e1dd2..8f7bbb4ce 100644 --- a/packages/runtime-vapor/src/scheduler.ts +++ b/packages/runtime-vapor/src/scheduler.ts @@ -32,7 +32,7 @@ let postFlushIndex = 0 const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise let currentFlushPromise: Promise | null = null -function queueJob(job: SchedulerJob) { +export function queueJob(job: SchedulerJob) { if (!(job.flags! & SchedulerJobFlags.QUEUED)) { if (job.id == null) { queue.push(job)