From: Evan You Date: Sat, 22 Jun 2024 13:05:15 +0000 (+0800) Subject: chore: Merge branch 'main' into minor X-Git-Tag: v3.5.0-alpha.3~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7d06ca3a832f60a36fd3fecfddcc7920c81267ab;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'main' into minor --- 7d06ca3a832f60a36fd3fecfddcc7920c81267ab diff --cc package.json index 4205f42fe9,545c053792..7dbd2117a9 --- a/package.json +++ b/package.json @@@ -1,7 -1,7 +1,7 @@@ { "private": true, - "version": "3.4.30", + "version": "3.5.0-alpha.2", - "packageManager": "pnpm@9.2.0", + "packageManager": "pnpm@9.3.0", "type": "module", "scripts": { "dev": "node scripts/dev.js", diff --cc packages/reactivity/__tests__/computed.spec.ts index dc3df3eaae,9a91eed638..fbe321dcae --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@@ -576,283 -696,75 +576,334 @@@ describe('reactivity/computed', () => expect(serializeInner(root)).toBe('Hello World') v.value += ' World' - expect(c.effect._dirtyLevel).toBe(DirtyLevels.Dirty) await nextTick() - expect(c.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect_Origin, - ) expect(serializeInner(root)).toBe('Hello World World World') - expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() + // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() + }) + + test('should not trigger if value did not change', () => { + const src = ref(0) + const c = computed(() => src.value % 2) + const spy = vi.fn() + effect(() => { + spy(c.value) + }) + expect(spy).toHaveBeenCalledTimes(1) + src.value = 2 + + // should not trigger + expect(spy).toHaveBeenCalledTimes(1) + + src.value = 3 + src.value = 5 + // should trigger because latest value changes + expect(spy).toHaveBeenCalledTimes(2) + }) + + test('chained computed trigger', () => { + const effectSpy = vi.fn() + const c1Spy = vi.fn() + const c2Spy = vi.fn() + + const src = ref(0) + const c1 = computed(() => { + c1Spy() + return src.value % 2 + }) + const c2 = computed(() => { + c2Spy() + return c1.value + 1 + }) + + effect(() => { + effectSpy(c2.value) + }) + + expect(c1Spy).toHaveBeenCalledTimes(1) + expect(c2Spy).toHaveBeenCalledTimes(1) + expect(effectSpy).toHaveBeenCalledTimes(1) + + src.value = 1 + expect(c1Spy).toHaveBeenCalledTimes(2) + expect(c2Spy).toHaveBeenCalledTimes(2) + expect(effectSpy).toHaveBeenCalledTimes(2) + }) + + test('chained computed avoid re-compute', () => { + const effectSpy = vi.fn() + const c1Spy = vi.fn() + const c2Spy = vi.fn() + + const src = ref(0) + const c1 = computed(() => { + c1Spy() + return src.value % 2 + }) + const c2 = computed(() => { + c2Spy() + return c1.value + 1 + }) + + effect(() => { + effectSpy(c2.value) + }) + + expect(effectSpy).toHaveBeenCalledTimes(1) + src.value = 2 + src.value = 4 + src.value = 6 + expect(c1Spy).toHaveBeenCalledTimes(4) + // c2 should not have to re-compute because c1 did not change. + expect(c2Spy).toHaveBeenCalledTimes(1) + // effect should not trigger because c2 did not change. + expect(effectSpy).toHaveBeenCalledTimes(1) + }) + + test('chained computed value invalidation', () => { + const effectSpy = vi.fn() + const c1Spy = vi.fn() + const c2Spy = vi.fn() + + const src = ref(0) + const c1 = computed(() => { + c1Spy() + return src.value % 2 + }) + const c2 = computed(() => { + c2Spy() + return c1.value + 1 + }) + + effect(() => { + effectSpy(c2.value) + }) + + expect(effectSpy).toHaveBeenCalledTimes(1) + expect(effectSpy).toHaveBeenCalledWith(1) + expect(c2.value).toBe(1) + + expect(c1Spy).toHaveBeenCalledTimes(1) + expect(c2Spy).toHaveBeenCalledTimes(1) + + src.value = 1 + // value should be available sync + expect(c2.value).toBe(2) + expect(c2Spy).toHaveBeenCalledTimes(2) + }) + + test('sync access of invalidated chained computed should not prevent final effect from running', () => { + const effectSpy = vi.fn() + const c1Spy = vi.fn() + const c2Spy = vi.fn() + + const src = ref(0) + const c1 = computed(() => { + c1Spy() + return src.value % 2 + }) + const c2 = computed(() => { + c2Spy() + return c1.value + 1 + }) + + effect(() => { + effectSpy(c2.value) + }) + expect(effectSpy).toHaveBeenCalledTimes(1) + + src.value = 1 + // sync access c2 + c2.value + expect(effectSpy).toHaveBeenCalledTimes(2) + }) + + it('computed should force track in untracked zone', () => { + const n = ref(0) + const spy1 = vi.fn() + const spy2 = vi.fn() + + let c: ComputedRef + effect(() => { + spy1() + pauseTracking() + n.value + c = computed(() => n.value + 1) + // access computed now to force refresh + c.value + effect(() => spy2(c.value)) + n.value + resetTracking() + }) + + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(1) + + n.value++ + // outer effect should not trigger + expect(spy1).toHaveBeenCalledTimes(1) + // inner effect should trigger + expect(spy2).toHaveBeenCalledTimes(2) + }) + + // not recommended behavior, but needed for backwards compatibility + // used in VueUse asyncComputed + it('computed side effect should be able trigger', () => { + const a = ref(false) + const b = ref(false) + const c = computed(() => { + a.value = true + return b.value + }) + effect(() => { + if (a.value) { + b.value = true + } + }) + expect(b.value).toBe(false) + // accessing c triggers change + c.value + expect(b.value).toBe(true) + expect(c.value).toBe(true) + }) + + it('chained computed should work when accessed before having subs', () => { + const n = ref(0) + const c = computed(() => n.value) + const d = computed(() => c.value + 1) + const spy = vi.fn() + + // access + d.value + + let dummy + effect(() => { + spy() + dummy = d.value + }) + expect(spy).toHaveBeenCalledTimes(1) + expect(dummy).toBe(1) + + n.value++ + expect(spy).toHaveBeenCalledTimes(2) + expect(dummy).toBe(2) + }) + + // #10236 + it('chained computed should still refresh after owner component unmount', async () => { + const a = ref(0) + const spy = vi.fn() + + const Child = { + setup() { + const b = computed(() => a.value + 1) + const c = computed(() => b.value + 1) + // access + c.value + onUnmounted(() => spy(c.value)) + return () => {} + }, + } + + const show = ref(true) + const Parent = { + setup() { + return () => (show.value ? h(Child) : null) + }, + } + + render(h(Parent), nodeOps.createElement('div')) + + a.value++ + show.value = false + + await nextTick() + expect(spy).toHaveBeenCalledWith(3) + }) + + // case: radix-vue `useForwardExpose` sets a template ref during mount, + // and checks for the element's closest form element in a computed. + // the computed is expected to only evaluate after mount. + it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => { + const calls: string[] = [] + const a = ref(0) + const b = computed(() => { + calls.push('b eval') + return a.value + 1 + }) + + const App = { + setup() { + onMounted(() => { + calls.push('mounted') + }) + return () => + h( + 'div', + { + ref: () => (a.value = 1), + }, + b.value, + ) + }, + } + + render(h(App), nodeOps.createElement('div')) + + await nextTick() + expect(calls).toMatchObject(['b eval', 'mounted', 'b eval']) }) + it('should chained computeds keep reactivity when computed effect happens', async () => { + const v = ref('Hello') + const c = computed(() => { + v.value += ' World' + return v.value + }) + const d = computed(() => c.value) + const e = computed(() => d.value) + const Comp = { + setup: () => { + return () => d.value + ' | ' + e.value + }, + } + const root = nodeOps.createElement('div') + + render(h(Comp), root) + await nextTick() + expect(serializeInner(root)).toBe('Hello World | Hello World') + + v.value += ' World' + await nextTick() + expect(serializeInner(root)).toBe( + 'Hello World World World | Hello World World World', + ) - expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() + }) + + it('should keep dirty level when side effect computed value changed', () => { + const v = ref(0) + const c = computed(() => { + v.value += 1 + return v.value + }) + const d = computed(() => { + return { d: c.value } + }) + + const Comp = { + setup: () => { + return () => { + return [d.value.d, d.value.d] + } + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + + expect(d.value.d).toBe(1) + expect(serializeInner(root)).toBe('11') - expect(c.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect_Origin, - ) - expect(d.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty_ComputedSideEffect) - expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() + }) + it('debug: onTrigger (ref)', () => { let events: DebuggerEvent[] = [] const onTrigger = vi.fn((e: DebuggerEvent) => { diff --cc packages/runtime-dom/__tests__/customElement.spec.ts index d8aad9c0af,62ba166b03..f503b2c028 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@@ -137,14 -138,10 +138,15 @@@ describe('defineCustomElement', () => describe('props', () => { const E = defineCustomElement({ - props: ['foo', 'bar', 'bazQux', 'value'], + props: { + foo: [String, null], + bar: Object, + bazQux: null, ++ value: null, + }, render() { return [ - h('div', null, this.foo), + h('div', null, this.foo || ''), h('div', null, this.bazQux || (this.bar && this.bar.x)), ] },