// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compile > bindings 1`] = `
-"import { template as _template, children as _children, createTextNode as _createTextNode, insert as _insert, effect as _effect, setText as _setText } from 'vue/vapor';
+"import { template as _template, children as _children, createTextNode as _createTextNode, insert as _insert, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div>count is <!>.</div>")
const { 0: [n3, { 1: [n2],}],} = _children(n0)
const n1 = _createTextNode(_ctx.count)
_insert(n1, n3, n2)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.count)
})
return n0
`;
exports[`compile > directives > v-pre > self-closing v-pre 1`] = `
-"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, watchEffect as _watchEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div><div><Comp></Comp></div>")
const { 1: [n2],} = _children(n0)
const n1 = _createTextNode(_ctx.bar)
_append(n2, n1)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.bar)
})
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n2, "id", undefined, _ctx.foo)
})
return n0
`;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
-"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, effect as _effect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, watchEffect as _watchEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div><div><Comp></Comp></div>")
const { 1: [n2],} = _children(n0)
const n1 = _createTextNode(_ctx.bar)
_append(n2, n1)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.bar)
})
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n2, "id", undefined, _ctx.foo)
})
return n0
`;
exports[`compile > dynamic root 1`] = `
-"import { fragment as _fragment, createTextNode as _createTextNode, append as _append, effect as _effect, setText as _setText } from 'vue/vapor';
+"import { fragment as _fragment, createTextNode as _createTextNode, append as _append, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor';
export function render(_ctx) {
const t0 = _fragment()
const n1 = _createTextNode(1)
const n2 = _createTextNode(2)
_append(n0, n1, n2)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, 1)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n2, undefined, 2)
})
return n0
`;
exports[`compile > dynamic root nodes and interpolation 1`] = `
-"import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, on as _on, effect as _effect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, on as _on, watchEffect as _watchEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<button>foo<!>foo</button>")
_insert(n2, n4, n5)
_append(n4, n3)
_on(n4, "click", (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.count)
_setText(n2, undefined, _ctx.count)
_setText(n3, undefined, _ctx.count)
const t0 = _fragment()
const n0 = t0()
- _effect(() => {
+ _watchEffect(() => {
_setText(n0, undefined, a + b.value)
})
return n0
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, key.value+1, undefined, _unref(foo)[key.value+1]())
})
return n0
`;
exports[`compile > static + dynamic root 1`] = `
-"import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, effect as _effect, setText as _setText } from 'vue/vapor';
+"import { template as _template, children as _children, createTextNode as _createTextNode, prepend as _prepend, insert as _insert, append as _append, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("3<!>6<!>9")
_insert([n3, n4], n0, n9)
_insert([n5, n6], n0, n10)
_append(n0, n7, n8)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, 1)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n2, undefined, 2)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n3, undefined, 4)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n4, undefined, 5)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n5, undefined, 7)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n6, undefined, 8)
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n7, undefined, 'A')
})
- _effect(() => {
+ _watchEffect(() => {
_setText(n8, undefined, 'B')
})
return n0
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler v-bind > .camel modifier 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, "fooBar", undefined, _ctx.id)
})
return n0
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id)
})
return n0
`;
exports[`compiler v-bind > .camel modifier w/ no expression 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, "fooBar", undefined, _ctx.fooBar)
})
return n0
`;
exports[`compiler v-bind > basic 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, "id", undefined, _ctx.id)
})
return n0
`;
exports[`compiler v-bind > dynamic arg 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, _ctx.id, undefined, _ctx.id)
})
return n0
`;
exports[`compiler v-bind > no expression (shorthand) 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, "camel-case", undefined, _ctx.camelCase)
})
return n0
`;
exports[`compiler v-bind > no expression 1`] = `
-"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setAttr(n1, "id", undefined, _ctx.id)
})
return n0
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v-html > should convert v-html to innerHTML 1`] = `
-"import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setHtml as _setHtml } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setHtml(n1, undefined, _ctx.code)
})
return n0
`;
exports[`v-html > should raise error and ignore children when v-html is present 1`] = `
-"import { template as _template, children as _children, effect as _effect, setHtml as _setHtml } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setHtml as _setHtml } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setHtml(n1, undefined, _ctx.test)
})
return n0
`;
exports[`v-on > dynamic arg 1`] = `
-"import { template as _template, children as _children, effect as _effect, on as _on } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_on(n1, _ctx.event, (...args) => (_ctx.handler && _ctx.handler(...args)))
})
return n0
`;
exports[`v-on > should transform click.middle 2`] = `
-"import { template as _template, children as _children, effect as _effect, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_on(n1, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"]))
})
return n0
`;
exports[`v-on > should transform click.right 2`] = `
-"import { template as _template, children as _children, effect as _effect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_on(n1, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"]), ["right"]))
})
return n0
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v-text > should convert v-text to textContent 1`] = `
-"import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.str)
})
return n0
`;
exports[`v-text > should raise error and ignore children when v-text is present 1`] = `
-"import { template as _template, children as _children, effect as _effect, setText as _setText } from 'vue/vapor';
+"import { template as _template, children as _children, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
- _effect(() => {
+ _watchEffect(() => {
_setText(n1, undefined, _ctx.test)
})
return n0
})
expect(code).matchSnapshot()
- expect(code).contains('effect')
+ expect(code).contains('watchEffect')
expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.fooBar)')
})
})
expect(code).matchSnapshot()
- expect(code).contains('effect')
+ expect(code).contains('watchEffect')
expect(code).contains(
`_setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id)`,
)
const { code, ir } = compileWithVOn(`<div v-on:[event]="handler"/>`)
expect(ir.vaporHelpers).contains('on')
- expect(ir.vaporHelpers).contains('effect')
+ expect(ir.vaporHelpers).contains('watchEffect')
expect(ir.helpers.size).toBe(0)
expect(ir.operation).toEqual([])
}
for (const { operations } of ir.effect) {
- pushNewline(`${vaporHelper('effect')}(() => {`)
+ pushNewline(`${vaporHelper('watchEffect')}(() => {`)
withIndent(() => {
for (const operation of operations) {
genOperation(operation, ctx)
--- /dev/null
+import { EffectScope, Ref, ref } from '@vue/reactivity'
+import {
+ onEffectCleanup,
+ watchEffect,
+ watchPostEffect,
+ watchSyncEffect,
+} from '../src/apiWatch'
+import { nextTick } from '../src/scheduler'
+import { defineComponent } from 'vue'
+import { render } from '../src/render'
+import { template } from '../src/template'
+
+let host: HTMLElement
+
+const initHost = () => {
+ host = document.createElement('div')
+ host.setAttribute('id', 'host')
+ document.body.appendChild(host)
+}
+beforeEach(() => {
+ initHost()
+})
+afterEach(() => {
+ host.remove()
+})
+
+describe('watchEffect and onEffectCleanup', () => {
+ test('basic', async () => {
+ let dummy = 0
+ let source: Ref<number>
+ const scope = new EffectScope()
+
+ scope.run(() => {
+ source = ref(0)
+ watchEffect((onCleanup) => {
+ source.value
+
+ onCleanup(() => (dummy += 2))
+ onEffectCleanup(() => (dummy += 3))
+ onEffectCleanup(() => (dummy += 5))
+ })
+ })
+ await nextTick()
+ expect(dummy).toBe(0)
+
+ scope.run(() => {
+ source.value++
+ })
+ await nextTick()
+ expect(dummy).toBe(10)
+
+ scope.run(() => {
+ source.value++
+ })
+ await nextTick()
+ expect(dummy).toBe(20)
+
+ scope.stop()
+ await nextTick()
+ expect(dummy).toBe(30)
+ })
+
+ test('nested call to watchEffect', async () => {
+ let dummy = 0
+ let source: Ref<number>
+ let double: Ref<number>
+ const scope = new EffectScope()
+
+ scope.run(() => {
+ source = ref(0)
+ double = ref(0)
+ watchEffect(() => {
+ double.value = source.value * 2
+ onEffectCleanup(() => (dummy += 2))
+ })
+ watchSyncEffect(() => {
+ double.value
+ onEffectCleanup(() => (dummy += 3))
+ })
+ })
+ await nextTick()
+ expect(dummy).toBe(0)
+
+ scope.run(() => source.value++)
+ await nextTick()
+ expect(dummy).toBe(5)
+
+ scope.run(() => source.value++)
+ await nextTick()
+ expect(dummy).toBe(10)
+
+ scope.stop()
+ await nextTick()
+ expect(dummy).toBe(15)
+ })
+
+ test('scheduling order', async () => {
+ const calls: string[] = []
+
+ const demo = defineComponent({
+ setup() {
+ const source = ref(0)
+ const change = () => source.value++
+
+ watchPostEffect(() => {
+ const current = source.value
+ calls.push(`post ${current}`)
+ onEffectCleanup(() => calls.push(`post cleanup ${current}`))
+ })
+ watchEffect(() => {
+ const current = source.value
+ calls.push(`pre ${current}`)
+ onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
+ })
+ watchSyncEffect(() => {
+ const current = source.value
+ calls.push(`sync ${current}`)
+ onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
+ })
+ const __returned__ = { source, change }
+ Object.defineProperty(__returned__, '__isScriptSetup', {
+ enumerable: false,
+ value: true,
+ })
+ return __returned__
+ },
+ })
+
+ demo.render = (_ctx: any) => {
+ const t0 = template('<div></div>')
+ watchEffect(() => {
+ const current = _ctx.source
+ calls.push(`render ${current}`)
+ onEffectCleanup(() => calls.push(`render cleanup ${current}`))
+ })
+ return t0()
+ }
+
+ const instance = render(demo as any, {}, '#host')
+ const { change } = instance.proxy as any
+
+ expect(calls).toEqual(['pre 0', 'sync 0', 'render 0'])
+ calls.length = 0
+
+ await nextTick()
+ expect(calls).toEqual(['post 0'])
+ calls.length = 0
+
+ change()
+ expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
+ calls.length = 0
+
+ await nextTick()
+ expect(calls).toEqual([
+ 'pre cleanup 0',
+ 'pre 1',
+ 'render cleanup 0',
+ 'render 1',
+ 'post cleanup 0',
+ 'post 1',
+ ])
+ })
+})
import {
template,
children,
- effect,
setText,
render,
ref,
unmountComponent,
+ watchEffect,
} from '../src'
import { afterEach, beforeEach, describe, expect } from 'vitest'
import { defineComponent } from '@vue/runtime-core'
const {
0: [n1],
} = children(n0)
- effect(() => {
+ watchEffect(() => {
setText(n1, void 0, count.value)
})
return n0
--- /dev/null
+import {
+ ComputedRef,
+ Ref,
+ isReactive,
+ isRef,
+ ReactiveEffect,
+ EffectScheduler,
+ DebuggerOptions,
+ getCurrentScope,
+ ReactiveFlags,
+} from '@vue/reactivity'
+import {
+ EMPTY_OBJ,
+ NOOP,
+ extend,
+ hasChanged,
+ isArray,
+ isFunction,
+ isMap,
+ isObject,
+ isPlainObject,
+ isSet,
+ remove,
+} from '@vue/shared'
+import { currentInstance } from './component'
+import {
+ type Scheduler,
+ getVaporSchedulerByFlushMode,
+ vaporPostScheduler,
+ vaporSyncScheduler,
+ SchedulerJob,
+} from './scheduler'
+import {
+ VaporErrorCodes,
+ callWithAsyncErrorHandling,
+ callWithErrorHandling,
+} from './errorHandling'
+import { warn } from './warning'
+
+export type WatchEffect = (onCleanup: OnCleanup) => void
+
+export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
+
+export type WatchCallback<V = any, OV = any> = (
+ value: V,
+ oldValue: OV,
+ onCleanup: OnCleanup,
+) => any
+
+type MapSources<T, Immediate> = {
+ [K in keyof T]: T[K] extends WatchSource<infer V>
+ ? Immediate extends true
+ ? V | undefined
+ : V
+ : T[K] extends object
+ ? Immediate extends true
+ ? T[K] | undefined
+ : T[K]
+ : never
+}
+
+type OnCleanup = (cleanupFn: () => void) => void
+
+export interface WatchOptionsBase extends DebuggerOptions {
+ flush?: 'pre' | 'post' | 'sync'
+}
+
+export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
+ immediate?: Immediate
+ deep?: boolean
+ once?: boolean
+}
+
+export type WatchStopHandle = () => void
+
+// Simple effect.
+export function watchEffect(
+ effect: WatchEffect,
+ options: WatchOptionsBase = EMPTY_OBJ,
+): WatchStopHandle {
+ const { flush } = options
+ return doWatch(effect, null, getVaporSchedulerByFlushMode(flush), options)
+}
+
+export function watchPostEffect(
+ effect: WatchEffect,
+ options?: DebuggerOptions,
+) {
+ return doWatch(
+ effect,
+ null,
+ vaporPostScheduler,
+ __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
+ )
+}
+
+export function watchSyncEffect(
+ effect: WatchEffect,
+ options?: DebuggerOptions,
+) {
+ return doWatch(
+ effect,
+ null,
+ vaporSyncScheduler,
+ __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
+ )
+}
+
+// initial value for watchers to trigger on undefined initial values
+const INITIAL_WATCHER_VALUE = {}
+
+type MultiWatchSources = (WatchSource<unknown> | object)[]
+
+// overload: array of multiple sources + cb
+export function watch<
+ T extends MultiWatchSources,
+ Immediate extends Readonly<boolean> = false,
+>(
+ sources: [...T],
+ cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+ options?: WatchOptions<Immediate>,
+): WatchStopHandle
+
+// overload: multiple sources w/ `as const`
+// watch([foo, bar] as const, () => {})
+// somehow [...T] breaks when the type is readonly
+export function watch<
+ T extends Readonly<MultiWatchSources>,
+ Immediate extends Readonly<boolean> = false,
+>(
+ source: T,
+ cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+ options?: WatchOptions<Immediate>,
+): WatchStopHandle
+
+// overload: single source + cb
+export function watch<T, Immediate extends Readonly<boolean> = false>(
+ source: WatchSource<T>,
+ cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+ options?: WatchOptions<Immediate>,
+): WatchStopHandle
+
+// overload: watching reactive object w/ cb
+export function watch<
+ T extends object,
+ Immediate extends Readonly<boolean> = false,
+>(
+ source: T,
+ cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+ options?: WatchOptions<Immediate>,
+): WatchStopHandle
+
+// implementation
+export function watch<T = any, Immediate extends Readonly<boolean> = false>(
+ source: T | WatchSource<T>,
+ cb: any,
+ options: WatchOptions<Immediate> = EMPTY_OBJ,
+): WatchStopHandle {
+ if (__DEV__ && !isFunction(cb)) {
+ warn(
+ `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
+ `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
+ `supports \`watch(source, cb, options?) signature.`,
+ )
+ }
+ const { flush } = options
+ return doWatch(
+ source as any,
+ cb,
+ getVaporSchedulerByFlushMode(flush),
+ options,
+ )
+}
+
+const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
+let activeEffect: ReactiveEffect | undefined = undefined
+
+// TODO: extract it to the reactivity package
+export function onEffectCleanup(cleanupFn: () => void) {
+ if (activeEffect) {
+ const cleanups =
+ cleanupMap.get(activeEffect) ||
+ cleanupMap.set(activeEffect, []).get(activeEffect)!
+ cleanups.push(cleanupFn)
+ }
+}
+
+export interface doWatchOptions<Immediate = boolean> extends DebuggerOptions {
+ immediate?: Immediate
+ deep?: boolean
+ once?: boolean
+}
+
+function doWatch(
+ source: WatchSource | WatchSource[] | WatchEffect | object,
+ cb: WatchCallback | null,
+ scheduler: Scheduler,
+ { immediate, deep, once, onTrack, onTrigger }: doWatchOptions = EMPTY_OBJ,
+): WatchStopHandle {
+ if (cb && once) {
+ const _cb = cb
+ cb = (...args) => {
+ _cb(...args)
+ unwatch()
+ }
+ }
+
+ if (__DEV__ && !cb) {
+ if (immediate !== undefined) {
+ warn(
+ `watch() "immediate" option is only respected when using the ` +
+ `watch(source, callback, options?) signature.`,
+ )
+ }
+ if (deep !== undefined) {
+ warn(
+ `watch() "deep" option is only respected when using the ` +
+ `watch(source, callback, options?) signature.`,
+ )
+ }
+ if (once !== undefined) {
+ warn(
+ `watch() "once" option is only respected when using the ` +
+ `watch(source, callback, options?) signature.`,
+ )
+ }
+ }
+
+ const warnInvalidSource = (s: unknown) => {
+ warn(
+ `Invalid watch source: `,
+ s,
+ `A watch source can only be a getter/effect function, a ref, ` +
+ `a reactive object, or an array of these types.`,
+ )
+ }
+
+ const instance =
+ getCurrentScope() === currentInstance?.scope ? currentInstance : null
+ // const instance = currentInstance
+ let getter: () => any
+ let forceTrigger = false
+ let isMultiSource = false
+
+ if (isRef(source)) {
+ getter = () => source.value
+ } else if (isReactive(source)) {
+ getter = () => source
+ deep = true
+ } else if (isArray(source)) {
+ getter = () =>
+ source.map((s) => {
+ if (isRef(s)) {
+ return s.value
+ } else if (isReactive(s)) {
+ return traverse(s)
+ } else if (isFunction(s)) {
+ return callWithErrorHandling(
+ s,
+ instance,
+ VaporErrorCodes.WATCH_GETTER,
+ )
+ } else {
+ __DEV__ && warnInvalidSource(s)
+ }
+ })
+ } else if (isFunction(source)) {
+ if (cb) {
+ // getter with cb
+ getter = () =>
+ callWithErrorHandling(source, instance, VaporErrorCodes.WATCH_GETTER)
+ } else {
+ // no cb -> simple effect
+ getter = () => {
+ if (instance && instance.isUnmounted) {
+ return
+ }
+ if (cleanup) {
+ cleanup()
+ }
+ const currentEffect = activeEffect
+ activeEffect = effect
+ try {
+ return callWithAsyncErrorHandling(
+ source,
+ instance,
+ VaporErrorCodes.WATCH_CALLBACK,
+ [onEffectCleanup],
+ )
+ } finally {
+ activeEffect = currentEffect
+ }
+ }
+ }
+ } else {
+ getter = NOOP
+ __DEV__ && warnInvalidSource(source)
+ }
+
+ if (cb && deep) {
+ const baseGetter = getter
+ getter = () => traverse(baseGetter())
+ }
+
+ // TODO: ssr
+ // if (__SSR__ && isInSSRComponentSetup) {
+ // }
+
+ let oldValue: any = isMultiSource
+ ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
+ : INITIAL_WATCHER_VALUE
+ const job: SchedulerJob = () => {
+ if (!effect.active || !effect.dirty) {
+ return
+ }
+ if (cb) {
+ // watch(source, cb)
+ const newValue = effect.run()
+ if (
+ deep ||
+ forceTrigger ||
+ (isMultiSource
+ ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
+ : hasChanged(newValue, oldValue))
+ ) {
+ // cleanup before running cb again
+ if (cleanup) {
+ cleanup()
+ }
+ const currentEffect = activeEffect
+ activeEffect = effect
+ try {
+ callWithAsyncErrorHandling(
+ cb,
+ instance,
+ VaporErrorCodes.WATCH_CALLBACK,
+ [
+ newValue,
+ // pass undefined as the old value when it's changed for the first time
+ oldValue === INITIAL_WATCHER_VALUE
+ ? undefined
+ : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
+ ? []
+ : oldValue,
+ onEffectCleanup,
+ ],
+ )
+ oldValue = newValue
+ } finally {
+ activeEffect = currentEffect
+ }
+ }
+ } else {
+ // watchEffect
+ effect.run()
+ }
+ }
+
+ // important: mark the job as a watcher callback so that scheduler knows
+ // it is allowed to self-trigger (#1727)
+ job.allowRecurse = !!cb
+
+ let effectScheduler: EffectScheduler = () =>
+ scheduler({
+ effect,
+ job,
+ instance: instance,
+ isInit: false,
+ })
+
+ const effect = new ReactiveEffect(getter, NOOP, effectScheduler)
+
+ const cleanup = (effect.onStop = () => {
+ const cleanups = cleanupMap.get(effect)
+ if (cleanups) {
+ cleanups.forEach((cleanup) => cleanup())
+ cleanupMap.delete(effect)
+ }
+ })
+
+ const unwatch = () => {
+ effect.stop()
+ if (instance && instance.scope) {
+ remove(instance.scope.effects!, effect)
+ }
+ }
+
+ if (__DEV__) {
+ effect.onTrack = onTrack
+ effect.onTrigger = onTrigger
+ }
+
+ // initial run
+ if (cb) {
+ if (immediate) {
+ job()
+ } else {
+ oldValue = effect.run()
+ }
+ } else {
+ scheduler({
+ effect,
+ job,
+ instance: instance,
+ isInit: true,
+ })
+ }
+
+ // TODO: ssr
+ // if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
+ return unwatch
+}
+
+export function traverse(value: unknown, seen?: Set<unknown>) {
+ if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
+ return value
+ }
+ seen = seen || new Set()
+ if (seen.has(value)) {
+ return value
+ }
+ seen.add(value)
+ if (isRef(value)) {
+ traverse(value.value, seen)
+ } else if (isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ traverse(value[i], seen)
+ }
+ } else if (isSet(value) || isMap(value)) {
+ value.forEach((v: any) => {
+ traverse(v, seen)
+ })
+ } else if (isPlainObject(value)) {
+ for (const key in value) {
+ traverse(value[key], seen)
+ }
+ }
+ return value
+}
component: FunctionalComponent | ObjectComponent
propsOptions: NormalizedPropsOptions
+ parent: ComponentInternalInstance | null
+
// TODO: type
proxy: Data | null
get isUnmounted(): boolean
isUnmountedRef: Ref<boolean>
isMountedRef: Ref<boolean>
- // TODO: registory of provides, appContext, lifecycles, ...
+ // TODO: registory of provides, lifecycles, ...
/**
* @internal
*/
scope: new EffectScope(true /* detached */)!,
component,
+ // TODO: registory of parent
+ parent: null,
+
// resolved props and emits options
propsOptions: normalizePropsOptions(component),
// emitsOptions: normalizeEmitsOptions(type, appContext), // TODO:
import { isFunction } from '@vue/shared'
import { currentInstance, type ComponentInternalInstance } from './component'
-import { effect } from './scheduler'
+import { watchEffect } from './apiWatch'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
callDirectiveHook(node, binding, 'created')
- effect(() => {
+ watchEffect(() => {
if (!instance.isMountedRef.value) return
callDirectiveHook(node, binding, 'updated')
})
--- /dev/null
+// These codes originate from a file of the same name in runtime-core,
+// duplicated during Vapor's early development to ensure its independence.
+// The ultimate aim is to uncouple this replicated code and
+// facilitate its shared use between two runtimes.
+
+import { VaporLifecycleHooks } from './apiLifecycle'
+import { type ComponentInternalInstance } from './component'
+import { isFunction, isPromise } from '@vue/shared'
+import { warn } from './warning'
+
+// contexts where user provided function may be executed, in addition to
+// lifecycle hooks.
+export enum VaporErrorCodes {
+ SETUP_FUNCTION,
+ RENDER_FUNCTION,
+ WATCH_GETTER,
+ WATCH_CALLBACK,
+ WATCH_CLEANUP,
+ NATIVE_EVENT_HANDLER,
+ COMPONENT_EVENT_HANDLER,
+ VNODE_HOOK,
+ DIRECTIVE_HOOK,
+ TRANSITION_HOOK,
+ APP_ERROR_HANDLER,
+ APP_WARN_HANDLER,
+ FUNCTION_REF,
+ ASYNC_COMPONENT_LOADER,
+ SCHEDULER,
+}
+
+export const ErrorTypeStrings: Record<
+ VaporLifecycleHooks | VaporErrorCodes,
+ string
+> = {
+ // [VaporLifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook',
+ [VaporLifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook',
+ [VaporLifecycleHooks.CREATED]: 'created hook',
+ [VaporLifecycleHooks.BEFORE_MOUNT]: 'beforeMount hook',
+ [VaporLifecycleHooks.MOUNTED]: 'mounted hook',
+ [VaporLifecycleHooks.BEFORE_UPDATE]: 'beforeUpdate hook',
+ [VaporLifecycleHooks.UPDATED]: 'updated',
+ [VaporLifecycleHooks.BEFORE_UNMOUNT]: 'beforeUnmount hook',
+ [VaporLifecycleHooks.UNMOUNTED]: 'unmounted hook',
+ [VaporLifecycleHooks.ACTIVATED]: 'activated hook',
+ [VaporLifecycleHooks.DEACTIVATED]: 'deactivated hook',
+ [VaporLifecycleHooks.ERROR_CAPTURED]: 'errorCaptured hook',
+ [VaporLifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
+ [VaporLifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
+ [VaporErrorCodes.SETUP_FUNCTION]: 'setup function',
+ [VaporErrorCodes.RENDER_FUNCTION]: 'render function',
+ [VaporErrorCodes.WATCH_GETTER]: 'watcher getter',
+ [VaporErrorCodes.WATCH_CALLBACK]: 'watcher callback',
+ [VaporErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
+ [VaporErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
+ [VaporErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
+ [VaporErrorCodes.VNODE_HOOK]: 'vnode hook',
+ [VaporErrorCodes.DIRECTIVE_HOOK]: 'directive hook',
+ [VaporErrorCodes.TRANSITION_HOOK]: 'transition hook',
+ [VaporErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
+ [VaporErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
+ [VaporErrorCodes.FUNCTION_REF]: 'ref function',
+ [VaporErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
+ [VaporErrorCodes.SCHEDULER]:
+ 'scheduler flush. This is likely a Vue internals bug. ' +
+ 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core',
+}
+
+export type ErrorTypes = VaporLifecycleHooks | VaporErrorCodes
+
+export function callWithErrorHandling(
+ fn: Function,
+ instance: ComponentInternalInstance | null,
+ type: ErrorTypes,
+ args?: unknown[],
+) {
+ let res
+ try {
+ res = args ? fn(...args) : fn()
+ } catch (err) {
+ handleError(err, instance, type)
+ }
+ return res
+}
+
+export function callWithAsyncErrorHandling(
+ fn: Function | Function[],
+ instance: ComponentInternalInstance | null,
+ type: ErrorTypes,
+ args?: unknown[],
+): any[] {
+ if (isFunction(fn)) {
+ const res = callWithErrorHandling(fn, instance, type, args)
+ if (res && isPromise(res)) {
+ res.catch((err) => {
+ handleError(err, instance, type)
+ })
+ }
+ return res
+ }
+
+ const values = []
+ for (let i = 0; i < fn.length; i++) {
+ values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
+ }
+ return values
+}
+
+export function handleError(
+ err: unknown,
+ instance: ComponentInternalInstance | null,
+ type: ErrorTypes,
+ throwInDev = true,
+) {
+ if (instance) {
+ let cur = instance.parent
+ // the exposed instance is the render proxy to keep it consistent with 2.x
+ const exposedInstance = ('proxy' in instance && instance.proxy) || null
+ // in production the hook receives only the error code
+ const errorInfo = __DEV__
+ ? ErrorTypeStrings[type]
+ : `https://vuejs.org/errors/#runtime-${type}`
+ while (cur) {
+ const errorCapturedHooks = 'ec' in cur ? cur.ec : null
+ if (errorCapturedHooks) {
+ for (let i = 0; i < errorCapturedHooks.length; i++) {
+ if (
+ errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
+ ) {
+ return
+ }
+ }
+ }
+ cur = cur.parent
+ }
+
+ // TODO: need appContext interface
+ // app-level handling
+ // const appErrorHandler = instance.appContext?.config.errorHandler
+ // if (appErrorHandler) {
+ // callWithErrorHandling(
+ // appErrorHandler,
+ // null,
+ // ErrorCodes.APP_ERROR_HANDLER,
+ // [err, exposedInstance, errorInfo],
+ // )
+ // return
+ // }
+ }
+ logError(err, type, throwInDev)
+}
+
+function logError(err: unknown, type: ErrorTypes, throwInDev = true) {
+ if (__DEV__) {
+ const info = ErrorTypeStrings[type]
+ warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
+ // crash in dev by default so it's more noticeable
+ if (throwInDev) {
+ throw err
+ } else if (!__TEST__) {
+ console.error(err)
+ }
+ } else {
+ // recover in prod to reduce the impact on end-user
+ console.error(err)
+ }
+}
export * from './render'
export * from './template'
export * from './scheduler'
+export * from './apiWatch'
export * from './directive'
export * from './dom'
export * from './directives/vShow'
export type Fragment = { nodes: Block; anchor: Node }
export type BlockFn = (props: any, ctx: any) => Block
+let isRenderingActivity = false
+export function getIsRendering() {
+ return isRenderingActivity
+}
+
export function render(
comp: Component,
props: Data,
let block: Block | null = null
if (state && '__isScriptSetup' in state) {
instance.setupState = proxyRefs(state)
- block = component.render(instance.proxy)
+ const currentlyRenderingActivity = isRenderingActivity
+ isRenderingActivity = true
+ try {
+ block = component.render(instance.proxy)
+ } finally {
+ isRenderingActivity = currentlyRenderingActivity
+ }
} else {
block = state as Block
}
import { ReactiveEffect } from '@vue/reactivity'
+import { ComponentInternalInstance } from './component'
+import { getIsRendering } from '.'
-const p = Promise.resolve()
+export interface SchedulerJob extends Function {
+ id?: number
+ pre?: boolean
+ active?: boolean
+ computed?: boolean
+ /**
+ * Indicates whether the effect is allowed to recursively trigger itself
+ * when managed by the scheduler.
+ *
+ * By default, a job cannot trigger itself because some built-in method calls,
+ * e.g. Array.prototype.push actually performs reads as well (#1740) which
+ * can lead to confusing infinite loops.
+ * The allowed cases are component update functions and watch callbacks.
+ * Component update functions may update child component props, which in turn
+ * trigger flush: "pre" watch callbacks that mutates state that the parent
+ * relies on (#1801). Watch callbacks doesn't track its dependencies so if it
+ * triggers itself again, it's likely intentional and it is the user's
+ * responsibility to perform recursive state mutation that eventually
+ * stabilizes (#1727).
+ */
+ allowRecurse?: boolean
+ /**
+ * Attached by renderer.ts when setting up a component's render effect
+ * Used to obtain component information when reporting max recursive updates.
+ * dev only.
+ */
+ ownerInstance?: ComponentInternalInstance
+}
+
+export type SchedulerJobs = SchedulerJob | SchedulerJob[]
+
+export type QueueEffect = (
+ cb: SchedulerJobs,
+ suspense: ComponentInternalInstance | null,
+) => void
+
+export type Scheduler = (context: {
+ effect: ReactiveEffect
+ job: SchedulerJob
+ instance: ComponentInternalInstance | null
+ isInit: boolean
+}) => void
+
+let isFlushing = false
+let isFlushPending = false
+
+// TODO: The queues in Vapor need to be merged with the queues in Core.
+// this is a temporary solution, the ultimate goal is to support
+// the mixed use of vapor components and default components.
+const queue: SchedulerJob[] = []
+let flushIndex = 0
+
+// TODO: The queues in Vapor need to be merged with the queues in Core.
+// this is a temporary solution, the ultimate goal is to support
+// the mixed use of vapor components and default components.
+const pendingPostFlushCbs: SchedulerJob[] = []
+let activePostFlushCbs: SchedulerJob[] | null = null
+let postFlushIndex = 0
+
+const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
+let currentFlushPromise: Promise<void> | null = null
+
+function queueJob(job: SchedulerJob) {
+ if (
+ !queue.length ||
+ !queue.includes(
+ job,
+ isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
+ )
+ ) {
+ if (job.id == null) {
+ queue.push(job)
+ } else {
+ queue.splice(findInsertionIndex(job.id), 0, job)
+ }
+ queueFlush()
+ }
+}
+
+export function queuePostRenderEffect(cb: SchedulerJob) {
+ if (
+ !activePostFlushCbs ||
+ !activePostFlushCbs.includes(
+ cb,
+ cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
+ )
+ ) {
+ pendingPostFlushCbs.push(cb)
+ }
+ queueFlush()
+}
+
+function queueFlush() {
+ if (!isFlushing && !isFlushPending) {
+ isFlushPending = true
+ currentFlushPromise = resolvedPromise.then(flushJobs)
+ }
+}
+
+function flushPostFlushCbs() {
+ if (!pendingPostFlushCbs.length) return
+
+ const deduped = [...new Set(pendingPostFlushCbs)]
+ pendingPostFlushCbs.length = 0
+
+ // #1947 already has active queue, nested flushPostFlushCbs call
+ if (activePostFlushCbs) {
+ activePostFlushCbs.push(...deduped)
+ return
+ }
+
+ activePostFlushCbs = deduped
+
+ activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
+
+ for (
+ postFlushIndex = 0;
+ postFlushIndex < activePostFlushCbs.length;
+ postFlushIndex++
+ ) {
+ activePostFlushCbs[postFlushIndex]()
+ }
+ activePostFlushCbs = null
+ postFlushIndex = 0
+}
+
+// TODO: dev mode and checkRecursiveUpdates
+function flushJobs() {
+ isFlushPending = false
+ isFlushing = true
-let queued: any[] | undefined
+ // Sort queue before flush.
+ // This ensures that:
+ // 1. Components are updated from parent to child. (because parent is always
+ // created before the child so its render effect will have smaller
+ // priority number)
+ // 2. If a component is unmounted during a parent component's update,
+ // its update can be skipped.
+ queue.sort(comparator)
-function queue(fn: any) {
- if (!queued) {
- queued = [fn]
- p.then(flush)
+ try {
+ for (let i = 0; i < queue!.length; i++) {
+ queue![i]()
+ }
+ } finally {
+ flushIndex = 0
+ queue.length = 0
+
+ flushPostFlushCbs()
+
+ isFlushing = false
+ currentFlushPromise = null
+ // some postFlushCb queued jobs!
+ // keep flushing until it drains.
+ if (queue.length || pendingPostFlushCbs.length) {
+ flushJobs()
+ }
+ }
+}
+
+export function nextTick<T = void, R = void>(
+ this: T,
+ fn?: (this: T) => R,
+): Promise<Awaited<R>> {
+ const p = currentFlushPromise || resolvedPromise
+ return fn ? p.then(this ? fn.bind(this) : fn) : p
+}
+
+// #2768
+// Use binary-search to find a suitable position in the queue,
+// so that the queue maintains the increasing order of job's id,
+// which can prevent the job from being skipped and also can avoid repeated patching.
+function findInsertionIndex(id: number) {
+ // the start index should be `flushIndex + 1`
+ let start = flushIndex + 1
+ let end = queue.length
+
+ while (start < end) {
+ const middle = (start + end) >>> 1
+ const middleJob = queue[middle]
+ const middleJobId = getId(middleJob)
+ if (middleJobId < id || (middleJobId === id && middleJob.pre)) {
+ start = middle + 1
+ } else {
+ end = middle
+ }
+ }
+
+ return start
+}
+
+const getId = (job: SchedulerJob): number =>
+ job.id == null ? Infinity : job.id
+
+const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
+ const diff = getId(a) - getId(b)
+ if (diff === 0) {
+ if (a.pre && !b.pre) return -1
+ if (b.pre && !a.pre) return 1
+ }
+ return diff
+}
+
+export function getVaporSchedulerByFlushMode(
+ flush?: 'pre' | 'post' | 'sync',
+): Scheduler {
+ if (flush === 'post') {
+ return vaporPostScheduler
+ }
+ if (flush === 'sync') {
+ return vaporSyncScheduler
+ }
+ if (getIsRendering()) {
+ return vaporRenderingScheduler
+ }
+ // default: 'pre'
+ return vaporPreScheduler
+}
+
+export const vaporSyncScheduler: Scheduler = ({ isInit, effect, job }) => {
+ if (isInit) {
+ effect.run()
} else {
- queued.push(fn)
+ job()
}
}
-function flush() {
- for (let i = 0; i < queued!.length; i++) {
- queued![i]()
+export const vaporPreScheduler: Scheduler = ({
+ isInit,
+ effect,
+ instance,
+ job,
+}) => {
+ if (isInit) {
+ effect.run()
+ } else {
+ job.pre = true
+ if (instance) job.id = instance.uid
+ queueJob(job)
}
- queued = undefined
}
-export const nextTick = (fn?: any) => (fn ? p.then(fn) : p)
+export const vaporRenderingScheduler: Scheduler = ({
+ isInit,
+ effect,
+ instance,
+ job,
+}) => {
+ if (isInit) {
+ effect.run()
+ } else {
+ job.pre = false
+ if (instance) job.id = instance.uid
+ queueJob(job)
+ }
+}
-export function effect(fn: any) {
- let run: () => void
- const e = new ReactiveEffect(fn, () => queue(run))
- run = e.run.bind(e)
- run()
+export const vaporPostScheduler: Scheduler = ({ isInit, effect, job }) => {
+ if (isInit) {
+ queuePostRenderEffect(effect.run.bind(effect))
+ } else {
+ queuePostRenderEffect(job)
+ }
}
--- /dev/null
+export function warn(msg: string, ...args: any[]) {
+ console.warn(`[Vue warn] ${msg}`, ...args)
+}
on,
ref,
template,
- effect,
+ watchEffect,
setText,
render as renderComponent // TODO:
} from '@vue/vapor'
0: [n1]
} = children(n0)
on(n1, 'click', _ctx.handleClick)
- effect(() => {
+ watchEffect(() => {
setText(n1, void 0, _ctx.count)
})
const {
0: [n1]
} = children(n0)
- effect(() => {
+ watchEffect(() => {
setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble)
})
return n0
--- /dev/null
+<script setup lang="ts">
+import { onEffectCleanup, ref, watch, watchEffect, watchPostEffect, watchSyncEffect } from 'vue/vapor'
+
+const source = ref(0)
+const add = () => source.value++
+
+watchPostEffect(() => {
+ const current = source.value
+ console.log('post', current)
+ onEffectCleanup(() => console.log('cleanup post', current))
+})
+
+watchEffect(() => {
+ const current = source.value
+ console.log('pre', current)
+ onEffectCleanup(() => console.log('cleanup pre', current))
+})
+
+watchSyncEffect(() => {
+ const current = source.value
+ console.log('sync', current)
+ onEffectCleanup(() => console.log('cleanup sync', current))
+})
+
+watch(source, (value, oldValue) => {
+ console.log('sync watch', value, 'oldValue:', oldValue)
+ onEffectCleanup(() => console.log('cleanup sync watch', value))
+})
+
+const onUpdate = (arg: any) => {
+ const current = source.value
+ console.log('render', current)
+ onEffectCleanup(() => console.log('cleanup render', current))
+ return arg
+}
+</script>
+
+<template>
+ <div>
+ <p>Please check the console</p>
+ <div>
+ <button @click="add">
+ Add
+ </button>
+ |
+ <span>{{ onUpdate(source) }}</span>
+ </div>
+ </div>
+</template>
+
+<style>
+.red {
+ color: red;
+}
+
+html {
+ color-scheme: dark;
+ background-color: #000;
+ padding: 10px;
+}
+</style>