diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 3693cd197..e658e4a2f 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -1,7 +1,7 @@ // 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, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor'; +"import { template as _template, children as _children, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
count is .
") @@ -9,7 +9,7 @@ export function render(_ctx) { const { 0: [n3, { 1: [n2],}],} = _children(n0) const n1 = _createTextNode(_ctx.count) _insert(n1, n3, n2) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.count) }) return n0 @@ -121,7 +121,7 @@ export function render(_ctx) { `; exports[`compile > directives > v-pre > self-closing v-pre 1`] = ` -"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'; +"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, renderEffect as _renderEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") @@ -129,10 +129,10 @@ export function render(_ctx) { const { 1: [n2],} = _children(n0) const n1 = _createTextNode(_ctx.bar) _append(n2, n1) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.bar) }) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n2, "id", undefined, _ctx.foo) }) return n0 @@ -140,7 +140,7 @@ export function render(_ctx) { `; 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, watchEffect as _watchEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, createTextNode as _createTextNode, append as _append, renderEffect as _renderEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
{{ bar }}
") @@ -148,10 +148,10 @@ export function render(_ctx) { const { 1: [n2],} = _children(n0) const n1 = _createTextNode(_ctx.bar) _append(n2, n1) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.bar) }) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n2, "id", undefined, _ctx.foo) }) return n0 @@ -159,7 +159,7 @@ export function render(_ctx) { `; exports[`compile > dynamic root 1`] = ` -"import { fragment as _fragment, createTextNode as _createTextNode, append as _append, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor'; +"import { fragment as _fragment, createTextNode as _createTextNode, append as _append, renderEffect as _renderEffect, setText as _setText } from 'vue/vapor'; export function render(_ctx) { const t0 = _fragment() @@ -168,10 +168,10 @@ export function render(_ctx) { const n1 = _createTextNode(1) const n2 = _createTextNode(2) _append(n0, n1, n2) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, 1) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n2, undefined, 2) }) return n0 @@ -179,7 +179,7 @@ export function render(_ctx) { `; 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, watchEffect as _watchEffect, 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, renderEffect as _renderEffect, setText as _setText, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("") @@ -192,7 +192,7 @@ export function render(_ctx) { _insert(n2, n4, n5) _append(n4, n3) _on(n4, "click", (...args) => (_ctx.handleClick && _ctx.handleClick(...args))) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.count) _setText(n2, undefined, _ctx.count) _setText(n3, undefined, _ctx.count) @@ -207,7 +207,7 @@ exports[`compile > expression parsing > interpolation 1`] = ` const t0 = _fragment() const n0 = t0() - _watchEffect(() => { + _renderEffect(() => { _setText(n0, undefined, a + b.value) }) return n0 @@ -219,7 +219,7 @@ exports[`compile > expression parsing > v-bind 1`] = ` const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, key.value+1, undefined, _unref(foo)[key.value+1]()) }) return n0 @@ -237,7 +237,7 @@ export function render(_ctx) { `; 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, watchEffect as _watchEffect, 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, renderEffect as _renderEffect, setText as _setText } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("369") @@ -255,28 +255,28 @@ export function render(_ctx) { _insert([n3, n4], n0, n9) _insert([n5, n6], n0, n10) _append(n0, n7, n8) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, 1) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n2, undefined, 2) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n3, undefined, 4) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n4, undefined, 5) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n5, undefined, 7) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n6, undefined, 8) }) - _watchEffect(() => { + _renderEffect(() => { _setText(n7, undefined, 'A') }) - _watchEffect(() => { + _renderEffect(() => { _setText(n8, undefined, 'B') }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 63936f19e..fa70b0f37 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -1,13 +1,13 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler v-bind > .camel modifier 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, "fooBar", undefined, _ctx.id) }) return n0 @@ -21,7 +21,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id) }) return n0 @@ -29,13 +29,13 @@ export function render(_ctx) { `; exports[`compiler v-bind > .camel modifier w/ no expression 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, "fooBar", undefined, _ctx.fooBar) }) return n0 @@ -43,13 +43,13 @@ export function render(_ctx) { `; exports[`compiler v-bind > basic 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, "id", undefined, _ctx.id) }) return n0 @@ -57,13 +57,13 @@ export function render(_ctx) { `; exports[`compiler v-bind > dynamic arg 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, _ctx.id, undefined, _ctx.id) }) return n0 @@ -71,13 +71,13 @@ export function render(_ctx) { `; exports[`compiler v-bind > no expression (shorthand) 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, "camel-case", undefined, _ctx.camelCase) }) return n0 @@ -85,13 +85,13 @@ export function render(_ctx) { `; exports[`compiler v-bind > no expression 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setAttr as _setAttr } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setAttr(n1, "id", undefined, _ctx.id) }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap index e1da7d157..865e46319 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap @@ -1,13 +1,13 @@ // 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, watchEffect as _watchEffect, setHtml as _setHtml } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setHtml as _setHtml } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setHtml(n1, undefined, _ctx.code) }) return n0 @@ -15,13 +15,13 @@ export function render(_ctx) { `; exports[`v-html > should raise error and ignore children when v-html is present 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setHtml as _setHtml } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setHtml as _setHtml } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setHtml(n1, undefined, _ctx.test) }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap index c1f85b96a..efea7f51d 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap @@ -13,13 +13,13 @@ export function render(_ctx) { `; exports[`v-on > dynamic arg 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _on(n1, _ctx.event, (...args) => (_ctx.handler && _ctx.handler(...args))) }) return n0 @@ -109,13 +109,13 @@ export function render(_ctx) { `; exports[`v-on > should transform click.middle 2`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _on(n1, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"])) }) return n0 @@ -135,31 +135,19 @@ export function render(_ctx) { `; exports[`v-on > should transform click.right 2`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _on(n1, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"]), ["right"])) }) return n0 }" `; -exports[`v-on > should wrap as function if expression is inline statement 1`] = ` -"import { template as _template, children as _children, on as _on } from 'vue/vapor'; - -export function render(_ctx) { - const t0 = _template("
") - const n0 = t0() - const { 0: [n1],} = _children(n0) - _on(n1, "click", (...args) => (_ctx.i++ && _ctx.i++(...args))) - return n0 -}" -`; - exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = ` "import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap index 8beed48c7..0ae00ce44 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap @@ -1,13 +1,13 @@ // 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, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setText as _setText } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.str) }) return n0 @@ -15,13 +15,13 @@ export function render(_ctx) { `; exports[`v-text > should raise error and ignore children when v-text is present 1`] = ` -"import { template as _template, children as _children, watchEffect as _watchEffect, setText as _setText } from 'vue/vapor'; +"import { template as _template, children as _children, renderEffect as _renderEffect, setText as _setText } from 'vue/vapor'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _watchEffect(() => { + _renderEffect(() => { _setText(n1, undefined, _ctx.test) }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index b6ef80f30..3c7932684 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -210,7 +210,7 @@ describe('compiler v-bind', () => { }) expect(code).matchSnapshot() - expect(code).contains('watchEffect') + expect(code).contains('renderEffect') expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.fooBar)') }) @@ -230,7 +230,7 @@ describe('compiler v-bind', () => { }) expect(code).matchSnapshot() - expect(code).contains('watchEffect') + expect(code).contains('renderEffect') expect(code).contains( `_setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id)`, ) diff --git a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts index 691c31467..08a73b430 100644 --- a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts @@ -102,7 +102,7 @@ describe('v-on', () => { const { code, ir } = compileWithVOn(`
`) expect(ir.vaporHelpers).contains('on') - expect(ir.vaporHelpers).contains('watchEffect') + expect(ir.vaporHelpers).contains('renderEffect') expect(ir.helpers.size).toBe(0) expect(ir.operation).toEqual([]) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 7d0cea4e6..6caf9a0d0 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -293,7 +293,7 @@ export function generate( } for (const { operations } of ir.effect) { - pushNewline(`${vaporHelper('watchEffect')}(() => {`) + pushNewline(`${vaporHelper('renderEffect')}(() => {`) withIndent(() => { for (const operation of operations) { genOperation(operation, ctx) diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts index 741a816b1..228c59cb2 100644 --- a/packages/runtime-vapor/__tests__/apiWatch.spec.ts +++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts @@ -1,28 +1,12 @@ -import { EffectScope, type Ref, ref } from '@vue/reactivity' +import type { Ref } from '@vue/reactivity' import { + EffectScope, + nextTick, onEffectCleanup, + ref, 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() -}) +} from '../src' describe('watchEffect and onEffectCleanup', () => { test('basic', async () => { @@ -93,71 +77,4 @@ describe('watchEffect and onEffectCleanup', () => { 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('
') - 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.setupState 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', - ]) - }) }) diff --git a/packages/runtime-vapor/__tests__/renderWatch.spec.ts b/packages/runtime-vapor/__tests__/renderWatch.spec.ts new file mode 100644 index 000000000..0d43ad90f --- /dev/null +++ b/packages/runtime-vapor/__tests__/renderWatch.spec.ts @@ -0,0 +1,133 @@ +import { defineComponent } from 'vue' +import { + nextTick, + onEffectCleanup, + ref, + render, + renderEffect, + renderWatch, + template, + watchEffect, + watchPostEffect, + watchSyncEffect, +} from '../src' + +let host: HTMLElement + +const initHost = () => { + host = document.createElement('div') + host.setAttribute('id', 'host') + document.body.appendChild(host) +} +beforeEach(() => { + initHost() +}) +afterEach(() => { + host.remove() +}) + +describe('renderWatch', () => { + test('effect', async () => { + let dummy: any + const source = ref(0) + renderEffect(() => { + dummy = source.value + }) + await nextTick() + expect(dummy).toBe(0) + source.value++ + await nextTick() + expect(dummy).toBe(1) + }) + + test('watch', async () => { + let dummy: any + const source = ref(0) + renderWatch(source, () => { + dummy = source.value + }) + await nextTick() + expect(dummy).toBe(undefined) + source.value++ + await nextTick() + expect(dummy).toBe(1) + }) + + test('scheduling order', async () => { + const calls: string[] = [] + + const demo = defineComponent({ + setup() { + const source = ref(0) + const renderSource = ref(0) + const change = () => source.value++ + const changeRender = () => renderSource.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, renderSource, changeRender } + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true, + }) + return __returned__ + }, + }) + + demo.render = (_ctx: any) => { + const t0 = template('
') + renderEffect(() => { + const current = _ctx.renderSource + calls.push(`renderEffect ${current}`) + onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`)) + }) + renderWatch( + () => _ctx.renderSource, + (value) => { + calls.push(`renderWatch ${value}`) + onEffectCleanup(() => calls.push(`renderWatch cleanup ${value}`)) + }, + ) + return t0() + } + + const instance = render(demo as any, {}, '#host') + const { change, changeRender } = instance.setupState as any + + expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0']) + calls.length = 0 + + await nextTick() + expect(calls).toEqual(['post 0']) + calls.length = 0 + + changeRender() + change() + expect(calls).toEqual(['sync cleanup 0', 'sync 1']) + calls.length = 0 + + await nextTick() + expect(calls).toEqual([ + 'pre cleanup 0', + 'pre 1', + 'renderEffect cleanup 0', + 'renderEffect 1', + 'renderWatch 1', + 'post cleanup 0', + 'post 1', + ]) + }) +}) diff --git a/packages/runtime-vapor/src/apiWatch.ts b/packages/runtime-vapor/src/apiWatch.ts index ca03b9f87..2c7cd8f63 100644 --- a/packages/runtime-vapor/src/apiWatch.ts +++ b/packages/runtime-vapor/src/apiWatch.ts @@ -1,40 +1,21 @@ import { + type BaseWatchErrorCodes, + type BaseWatchOptions, type ComputedRef, type DebuggerOptions, - type EffectScheduler, - ReactiveEffect, - ReactiveFlags, type Ref, + baseWatch, getCurrentScope, - isReactive, - isRef, } from '@vue/reactivity' -import { - EMPTY_OBJ, - NOOP, - extend, - hasChanged, - isArray, - isFunction, - isMap, - isObject, - isPlainObject, - isSet, - remove, -} from '@vue/shared' +import { EMPTY_OBJ, NOOP, extend, isFunction, remove } from '@vue/shared' import { currentInstance } from './component' import { - type Scheduler, - type SchedulerJob, - getVaporSchedulerByFlushMode, - vaporPostScheduler, - vaporSyncScheduler, + type SchedulerFactory, + createVaporPostScheduler, + createVaporPreScheduler, + createVaporSyncScheduler, } from './scheduler' -import { - VaporErrorCodes, - callWithAsyncErrorHandling, - callWithErrorHandling, -} from './errorHandling' +import { handleError as handleErrorWithInstance } from './errorHandling' import { warn } from './warning' export type WatchEffect = (onCleanup: OnCleanup) => void @@ -76,10 +57,9 @@ export type WatchStopHandle = () => void // Simple effect. export function watchEffect( effect: WatchEffect, - options: WatchOptionsBase = EMPTY_OBJ, + options?: WatchOptionsBase, ): WatchStopHandle { - const { flush } = options - return doWatch(effect, null, getVaporSchedulerByFlushMode(flush), options) + return doWatch(effect, null, options) } export function watchPostEffect( @@ -89,7 +69,6 @@ export function watchPostEffect( return doWatch( effect, null, - vaporPostScheduler, __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' }, ) } @@ -101,16 +80,19 @@ export function watchSyncEffect( 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 | object)[] +// overload: single source + cb +export function watch = false>( + source: WatchSource, + cb: WatchCallback, + options?: WatchOptions, +): WatchStopHandle + // overload: array of multiple sources + cb export function watch< T extends MultiWatchSources, @@ -133,13 +115,6 @@ export function watch< options?: WatchOptions, ): WatchStopHandle -// overload: single source + cb -export function watch = false>( - source: WatchSource, - cb: WatchCallback, - options?: WatchOptions, -): WatchStopHandle - // overload: watching reactive object w/ cb export function watch< T extends object, @@ -154,7 +129,7 @@ export function watch< export function watch = false>( source: T | WatchSource, cb: any, - options: WatchOptions = EMPTY_OBJ, + options?: WatchOptions, ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( @@ -163,46 +138,33 @@ export function watch = false>( `supports \`watch(source, cb, options?) signature.`, ) } - const { flush } = options - return doWatch( - source as any, - cb, - getVaporSchedulerByFlushMode(flush), - options, - ) + return doWatch(source as any, cb, options) } -const cleanupMap: WeakMap 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) +function getScheduler(flush: WatchOptionsBase['flush']): SchedulerFactory { + if (flush === 'post') { + return createVaporPostScheduler } -} - -export interface doWatchOptions extends DebuggerOptions { - immediate?: Immediate - deep?: boolean - once?: boolean + if (flush === 'sync') { + return createVaporSyncScheduler + } + // default: 'pre' + return createVaporPreScheduler } function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, - scheduler: Scheduler, - { immediate, deep, once, onTrack, onTrigger }: doWatchOptions = EMPTY_OBJ, + options: WatchOptions = EMPTY_OBJ, ): WatchStopHandle { - if (cb && once) { - const _cb = cb - cb = (...args) => { - _cb(...args) - unwatch() - } + const { immediate, deep, flush, once } = options + + // TODO remove in 3.5 + if (__DEV__ && deep !== void 0 && typeof deep === 'number') { + warn( + `watch() "deep" option with number value will be used as watch depth in future versions. ` + + `Please use a boolean instead to avoid potential breakage.`, + ) } if (__DEV__ && !cb) { @@ -226,214 +188,43 @@ function doWatch( } } - 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 extendOptions: BaseWatchOptions = {} - const instance = - getCurrentScope() === currentInstance?.scope ? currentInstance : null - // const instance = currentInstance - let getter: () => any - let forceTrigger = false - let isMultiSource = false + if (__DEV__) extendOptions.onWarn = warn - 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 + let ssrCleanup: (() => void)[] | undefined + // TODO: SSR // if (__SSR__ && isInSSRComponentSetup) { + // if (flush === 'sync') { + // const ctx = useSSRContext()! + // ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) + // } else if (!cb || immediate) { + // // immediately watch or watchEffect + // extendOptions.once = true + // } else { + // // watch(source, cb) + // return NOOP + // } // } - 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 instance = currentInstance - const effect = new ReactiveEffect(getter, NOOP, effectScheduler) + extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => + handleErrorWithInstance(err, instance, type) + extendOptions.scheduler = getScheduler(flush)(instance) - const cleanup = (effect.onStop = () => { - const cleanups = cleanupMap.get(effect) - if (cleanups) { - cleanups.forEach((cleanup) => cleanup()) - cleanupMap.delete(effect) - } - }) + let effect = baseWatch(source, cb, extend({}, options, extendOptions)) - 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, - }) - } + const scope = getCurrentScope() + const unwatch = !effect + ? NOOP + : () => { + effect!.stop() + if (scope) { + remove(scope.effects, effect) + } + } - // TODO: ssr - // if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) + if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch } - -export function traverse(value: unknown, seen?: Set) { - 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 -} diff --git a/packages/runtime-vapor/src/errorHandling.ts b/packages/runtime-vapor/src/errorHandling.ts index 0867d4bd9..fa3954280 100644 --- a/packages/runtime-vapor/src/errorHandling.ts +++ b/packages/runtime-vapor/src/errorHandling.ts @@ -7,16 +7,20 @@ import type { ComponentInternalInstance } from './component' import { isFunction, isPromise } from '@vue/shared' import { warn } from './warning' import { VaporLifecycleHooks } from './enums' +import { BaseWatchErrorCodes } from '@vue/reactivity' // 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, + // The error codes for the watch have been transferred to the reactivity + // package along with baseWatch to maintain code compatibility. Hence, + // it is essential to keep these values unchanged. + // WATCH_GETTER, + // WATCH_CALLBACK, + // WATCH_CLEANUP, + NATIVE_EVENT_HANDLER = 5, COMPONENT_EVENT_HANDLER, VNODE_HOOK, DIRECTIVE_HOOK, @@ -28,10 +32,12 @@ export enum VaporErrorCodes { SCHEDULER, } -export const ErrorTypeStrings: Record< - VaporLifecycleHooks | VaporErrorCodes, - string -> = { +export type ErrorTypes = + | VaporLifecycleHooks + | VaporErrorCodes + | BaseWatchErrorCodes + +export const ErrorTypeStrings: Record = { // [VaporLifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook', [VaporLifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook', [VaporLifecycleHooks.CREATED]: 'created hook', @@ -48,9 +54,9 @@ export const ErrorTypeStrings: Record< [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', + [BaseWatchErrorCodes.WATCH_GETTER]: 'watcher getter', + [BaseWatchErrorCodes.WATCH_CALLBACK]: 'watcher callback', + [BaseWatchErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function', [VaporErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler', [VaporErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler', [VaporErrorCodes.VNODE_HOOK]: 'vnode hook', @@ -65,8 +71,6 @@ export const ErrorTypeStrings: Record< '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, diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index d34c9f483..c740912ef 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -29,6 +29,7 @@ export { // effect stop, ReactiveEffect, + onEffectCleanup, // effect scope effectScope, EffectScope, @@ -39,6 +40,7 @@ export { withModifiers, withKeys } from '@vue/runtime-dom' export * from './on' export * from './render' +export * from './renderWatch' export * from './template' export * from './scheduler' export * from './apiWatch' diff --git a/packages/runtime-vapor/src/renderWatch.ts b/packages/runtime-vapor/src/renderWatch.ts new file mode 100644 index 000000000..fd9385fc5 --- /dev/null +++ b/packages/runtime-vapor/src/renderWatch.ts @@ -0,0 +1,55 @@ +import { + type BaseWatchErrorCodes, + type BaseWatchOptions, + baseWatch, + getCurrentScope, +} from '@vue/reactivity' +import { NOOP, remove } from '@vue/shared' +import { currentInstance } from './component' +import { createVaporRenderingScheduler } from './scheduler' +import { handleError as handleErrorWithInstance } from './errorHandling' +import { warn } from './warning' + +type WatchStopHandle = () => void + +export function renderEffect(effect: () => void): WatchStopHandle { + return doWatch(effect) +} + +export function renderWatch( + source: any, + cb: (value: any, oldValue: any) => void, +): WatchStopHandle { + return doWatch(source as any, cb) +} + +function doWatch(source: any, cb?: any): WatchStopHandle { + const extendOptions: BaseWatchOptions = {} + + if (__DEV__) extendOptions.onWarn = warn + + // TODO: Life Cycle Hooks + + // TODO: SSR + // if (__SSR__) {} + + const instance = + getCurrentScope() === currentInstance?.scope ? currentInstance : null + + extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => + handleErrorWithInstance(err, instance, type) + extendOptions.scheduler = createVaporRenderingScheduler(instance) + + let effect = baseWatch(source, cb, extendOptions) + + const unwatch = !effect + ? NOOP + : () => { + effect!.stop() + if (instance && instance.scope) { + remove(instance.scope.effects!, effect) + } + } + + return unwatch +} diff --git a/packages/runtime-vapor/src/scheduler.ts b/packages/runtime-vapor/src/scheduler.ts index de511787e..2be470254 100644 --- a/packages/runtime-vapor/src/scheduler.ts +++ b/packages/runtime-vapor/src/scheduler.ts @@ -1,6 +1,5 @@ -import type { ReactiveEffect } from '@vue/reactivity' +import type { Scheduler } from '@vue/reactivity' import type { ComponentInternalInstance } from './component' -import { getIsRendering } from '.' export interface SchedulerJob extends Function { id?: number @@ -38,13 +37,6 @@ export type QueueEffect = ( suspense: ComponentInternalInstance | null, ) => void -export type Scheduler = (context: { - effect: ReactiveEffect - job: SchedulerJob - instance: ComponentInternalInstance | null - isInit: boolean -}) => void - let isFlushing = false let isFlushPending = false @@ -205,64 +197,46 @@ const comparator = (a: SchedulerJob, b: SchedulerJob): number => { 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 type SchedulerFactory = ( + instance: ComponentInternalInstance | null, +) => Scheduler -export const vaporSyncScheduler: Scheduler = ({ isInit, effect, job }) => { - if (isInit) { - effect.run() - } else { - job() +export const createVaporSyncScheduler: SchedulerFactory = + (instance) => (job, effect, isInit) => { + if (isInit) { + effect.run() + } else { + job() + } } -} -export const vaporPreScheduler: Scheduler = ({ - isInit, - effect, - instance, - job, -}) => { - if (isInit) { - effect.run() - } else { - job.pre = true - if (instance) job.id = instance.uid - queueJob(job) +export const createVaporPreScheduler: SchedulerFactory = + (instance) => (job, effect, isInit) => { + if (isInit) { + effect.run() + } else { + job.pre = true + if (instance) job.id = instance.uid + queueJob(job) + } } -} -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 const createVaporRenderingScheduler: SchedulerFactory = + (instance) => (job, effect, isInit) => { + if (isInit) { + effect.run() + } else { + job.pre = false + if (instance) job.id = instance.uid + queueJob(job) + } } -} -export const vaporPostScheduler: Scheduler = ({ isInit, effect, job }) => { - if (isInit) { - queuePostRenderEffect(effect.run.bind(effect)) - } else { - queuePostRenderEffect(job) +export const createVaporPostScheduler: SchedulerFactory = + (instance) => (job, effect, isInit) => { + if (isInit) { + queuePostRenderEffect(effect.run.bind(effect)) + } else { + queuePostRenderEffect(job) + } } -}