diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 02c09554e..3693cd197 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, 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("
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) - _effect(() => { + _watchEffect(() => { _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, 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("
") @@ -129,10 +129,10 @@ export function render(_ctx) { 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 @@ -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, 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("
{{ bar }}
") @@ -148,10 +148,10 @@ export function render(_ctx) { 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 @@ -159,7 +159,7 @@ export function render(_ctx) { `; 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() @@ -168,10 +168,10 @@ export function render(_ctx) { 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 @@ -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, 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("") @@ -192,7 +192,7 @@ export function render(_ctx) { _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) @@ -207,7 +207,7 @@ exports[`compile > expression parsing > interpolation 1`] = ` const t0 = _fragment() const n0 = t0() - _effect(() => { + _watchEffect(() => { _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) - _effect(() => { + _watchEffect(() => { _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, 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("369") @@ -255,28 +255,28 @@ export function render(_ctx) { _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 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 910d019cd..63936f19e 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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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 d3abf8903..e1da7d157 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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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 b1c8449cc..c1f85b96a 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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") 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 @@ -135,13 +135,13 @@ export function render(_ctx) { `; 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("
") 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 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 d18e398b1..8beed48c7 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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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, 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("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _effect(() => { + _watchEffect(() => { _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 c81f2e51a..31a1a8d0f 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('effect') + expect(code).contains('watchEffect') expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.fooBar)') }) @@ -230,7 +230,7 @@ describe('compiler v-bind', () => { }) expect(code).matchSnapshot() - expect(code).contains('effect') + expect(code).contains('watchEffect') 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 c731f30d0..d9076607f 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('effect') + expect(ir.vaporHelpers).contains('watchEffect') 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 b79ac2901..339b05d66 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('effect')}(() => {`) + pushNewline(`${vaporHelper('watchEffect')}(() => {`) 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 new file mode 100644 index 000000000..02f88dedb --- /dev/null +++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts @@ -0,0 +1,163 @@ +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 + 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 + let double: Ref + 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('
') + 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', + ]) + }) +}) diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index 549402a00..5699dd594 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -1,11 +1,11 @@ import { template, children, - effect, setText, render, ref, unmountComponent, + watchEffect, } from '../src' import { afterEach, beforeEach, describe, expect } from 'vitest' import { defineComponent } from '@vue/runtime-core' @@ -33,7 +33,7 @@ describe('component', () => { const { 0: [n1], } = children(n0) - effect(() => { + watchEffect(() => { setText(n1, void 0, count.value) }) return n0 diff --git a/packages/runtime-vapor/src/apiWatch.ts b/packages/runtime-vapor/src/apiWatch.ts new file mode 100644 index 000000000..ee88648a3 --- /dev/null +++ b/packages/runtime-vapor/src/apiWatch.ts @@ -0,0 +1,439 @@ +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 = Ref | ComputedRef | (() => T) + +export type WatchCallback = ( + value: V, + oldValue: OV, + onCleanup: OnCleanup, +) => any + +type MapSources = { + [K in keyof T]: T[K] extends WatchSource + ? 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 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 | object)[] + +// overload: array of multiple sources + cb +export function watch< + T extends MultiWatchSources, + Immediate extends Readonly = false, +>( + sources: [...T], + cb: WatchCallback, MapSources>, + options?: WatchOptions, +): 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, + Immediate extends Readonly = false, +>( + source: T, + cb: WatchCallback, MapSources>, + 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, + Immediate extends Readonly = false, +>( + source: T, + cb: WatchCallback, + options?: WatchOptions, +): WatchStopHandle + +// implementation +export function watch = false>( + source: T | WatchSource, + cb: any, + options: WatchOptions = 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 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 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) { + 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/component.ts b/packages/runtime-vapor/src/component.ts index 23b5f0770..63e1f2528 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -35,6 +35,8 @@ export interface ComponentInternalInstance { component: FunctionalComponent | ObjectComponent propsOptions: NormalizedPropsOptions + parent: ComponentInternalInstance | null + // TODO: type proxy: Data | null @@ -50,7 +52,7 @@ export interface ComponentInternalInstance { get isUnmounted(): boolean isUnmountedRef: Ref isMountedRef: Ref - // TODO: registory of provides, appContext, lifecycles, ... + // TODO: registory of provides, lifecycles, ... /** * @internal */ @@ -136,6 +138,9 @@ export const createComponentInstance = ( 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: diff --git a/packages/runtime-vapor/src/directive.ts b/packages/runtime-vapor/src/directive.ts index 3906eb723..0951cbeeb 100644 --- a/packages/runtime-vapor/src/directive.ts +++ b/packages/runtime-vapor/src/directive.ts @@ -1,6 +1,6 @@ import { isFunction } from '@vue/shared' import { currentInstance, type ComponentInternalInstance } from './component' -import { effect } from './scheduler' +import { watchEffect } from './apiWatch' export type DirectiveModifiers = Record @@ -95,7 +95,7 @@ export function withDirectives( callDirectiveHook(node, binding, 'created') - effect(() => { + watchEffect(() => { if (!instance.isMountedRef.value) return callDirectiveHook(node, binding, 'updated') }) diff --git a/packages/runtime-vapor/src/errorHandling.ts b/packages/runtime-vapor/src/errorHandling.ts new file mode 100644 index 000000000..7c0056512 --- /dev/null +++ b/packages/runtime-vapor/src/errorHandling.ts @@ -0,0 +1,166 @@ +// 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) + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 7f6165677..d34c9f483 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -41,6 +41,7 @@ export * from './on' export * from './render' export * from './template' export * from './scheduler' +export * from './apiWatch' export * from './directive' export * from './dom' export * from './directives/vShow' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index a2f505ecd..1f53978e8 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -17,6 +17,11 @@ export type ParentBlock = ParentNode | Node[] 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, @@ -53,7 +58,13 @@ export function mountComponent( 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 } diff --git a/packages/runtime-vapor/src/scheduler.ts b/packages/runtime-vapor/src/scheduler.ts index 876e2c45e..703da98f6 100644 --- a/packages/runtime-vapor/src/scheduler.ts +++ b/packages/runtime-vapor/src/scheduler.ts @@ -1,30 +1,268 @@ 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 +} -let queued: any[] | undefined +export type SchedulerJobs = SchedulerJob | SchedulerJob[] -function queue(fn: any) { - if (!queued) { - queued = [fn] - p.then(flush) +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 +let currentFlushPromise: Promise | 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 + + // 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) + + 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( + this: T, + fn?: (this: T) => R, +): Promise> { + 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 function effect(fn: any) { - let run: () => void - const e = new ReactiveEffect(fn, () => queue(run)) - run = e.run.bind(e) - run() +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 vaporPostScheduler: Scheduler = ({ isInit, effect, job }) => { + if (isInit) { + queuePostRenderEffect(effect.run.bind(effect)) + } else { + queuePostRenderEffect(job) + } } diff --git a/packages/runtime-vapor/src/warning.ts b/packages/runtime-vapor/src/warning.ts new file mode 100644 index 000000000..c6cbdfe8c --- /dev/null +++ b/packages/runtime-vapor/src/warning.ts @@ -0,0 +1,3 @@ +export function warn(msg: string, ...args: any[]) { + console.warn(`[Vue warn] ${msg}`, ...args) +} diff --git a/playground/src/props.js b/playground/src/props.js index b80768dcc..695800ecd 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -4,7 +4,7 @@ import { on, ref, template, - effect, + watchEffect, setText, render as renderComponent // TODO: } from '@vue/vapor' @@ -35,7 +35,7 @@ export default { 0: [n1] } = children(n0) on(n1, 'click', _ctx.handleClick) - effect(() => { + watchEffect(() => { setText(n1, void 0, _ctx.count) }) @@ -96,7 +96,7 @@ const child = { const { 0: [n1] } = children(n0) - effect(() => { + watchEffect(() => { setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble) }) return n0 diff --git a/playground/src/scheduler.vue b/playground/src/scheduler.vue new file mode 100644 index 000000000..a3019342a --- /dev/null +++ b/playground/src/scheduler.vue @@ -0,0 +1,61 @@ + + + + +