From 4c40cf3490898953aa477417f2173037fa18c786 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Fri, 12 Jan 2024 18:28:57 +0800 Subject: [PATCH 1/4] fix(reactivity): correct dirty assign in recursive render close #10082 --- packages/reactivity/src/effect.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 5ddf8e6f0ac..8876604bc7a 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -295,7 +295,9 @@ export function triggerEffects( } if ( effect._dirtyLevel < dirtyLevel && - (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty) + (!effect._runnings || + effect.allowRecurse || + dirtyLevel !== DirtyLevels.ComputedValueDirty) ) { const lastDirtyLevel = effect._dirtyLevel effect._dirtyLevel = dirtyLevel From c3c3ae8fe89969baf663ca4bd3963818c1d2a23a Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Fri, 12 Jan 2024 19:51:37 +0800 Subject: [PATCH 2/4] feat(test): add effect test --- packages/reactivity/__tests__/effect.spec.ts | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 9109177bf49..3021627aff8 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -3,16 +3,20 @@ import { type ReactiveEffectRunner, TrackOpTypes, TriggerOpTypes, + computed, effect, markRaw, reactive, readonly, + ref, shallowReactive, stop, toRaw, } from '../src/index' import { pauseScheduling, resetScheduling } from '../src/effect' import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' +import { Fragment, defineComponent, h, nextTick } from 'vue' +import { nodeOps, render, serialize } from '@vue/runtime-test' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -942,6 +946,46 @@ describe('reactivity/effect', () => { expect(ret1).toBe(ret2) }) + it('should set dirtyLevel when effect is allowRecurse and is running', async () => { + const Child = defineComponent({ + props: ['getComputed', 'addComputedDep'], + created() { + this.addComputedDep() + }, + render: (props: { + getComputed: () => boolean + addComputedDep: () => void + }) => { + return h('div', props.getComputed()) + }, + }) + + const Parent = defineComponent(() => { + const count = ref(0) + const moreThanOne = computed(() => { + return count.value > 1 + }) + const getComputed = () => moreThanOne.value + const addComputedDep = () => { + count.value++ + } + return () => + h(Fragment, [ + getComputed() ? h('span', 'moreThanOne') : null, + h(Child, { getComputed, addComputedDep }), + h(Child, { getComputed, addComputedDep }), + h(Child, { getComputed, addComputedDep }), + ]) + }) + + const root = nodeOps.createElement('div') + render(h(Parent), root) + await nextTick() + expect(serialize(root)).toBe( + '
moreThanOne
true
true
true
', + ) + }) + describe('readonly + reactive for Map', () => { test('should work with readonly(reactive(Map))', () => { const m = reactive(new Map()) From 189b97629c70579b7b160cbb33f0c412ab2b4e93 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Fri, 12 Jan 2024 19:57:12 +0800 Subject: [PATCH 3/4] feat(test): move test to computed.spec --- .../reactivity/__tests__/computed.spec.ts | 48 +++++++++++++++++++ packages/reactivity/__tests__/effect.spec.ts | 44 ----------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index f66935d4c8d..250768bdbc9 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1,3 +1,12 @@ +import { + Fragment, + defineComponent, + h, + nextTick, + nodeOps, + render, + serialize, +} from '@vue/runtime-test' import { type DebuggerEvent, ITERATE_KEY, @@ -451,4 +460,43 @@ describe('reactivity/computed', () => { v.value = 2 expect(fnSpy).toBeCalledTimes(2) }) + it('should set dirtyLevel when effect is allowRecurse and is running', async () => { + const Child = defineComponent({ + props: ['getComputed', 'addComputedDep'], + created() { + this.addComputedDep() + }, + render: (props: { + getComputed: () => boolean + addComputedDep: () => void + }) => { + return h('div', props.getComputed()) + }, + }) + + const Parent = defineComponent(() => { + const count = ref(0) + const moreThanOne = computed(() => { + return count.value > 1 + }) + const getComputed = () => moreThanOne.value + const addComputedDep = () => { + count.value++ + } + return () => + h(Fragment, [ + getComputed() ? h('span', 'moreThanOne') : null, + h(Child, { getComputed, addComputedDep }), + h(Child, { getComputed, addComputedDep }), + h(Child, { getComputed, addComputedDep }), + ]) + }) + + const root = nodeOps.createElement('div') + render(h(Parent), root) + await nextTick() + expect(serialize(root)).toBe( + '
moreThanOne
true
true
true
', + ) + }) }) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 3021627aff8..9109177bf49 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -3,20 +3,16 @@ import { type ReactiveEffectRunner, TrackOpTypes, TriggerOpTypes, - computed, effect, markRaw, reactive, readonly, - ref, shallowReactive, stop, toRaw, } from '../src/index' import { pauseScheduling, resetScheduling } from '../src/effect' import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' -import { Fragment, defineComponent, h, nextTick } from 'vue' -import { nodeOps, render, serialize } from '@vue/runtime-test' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -946,46 +942,6 @@ describe('reactivity/effect', () => { expect(ret1).toBe(ret2) }) - it('should set dirtyLevel when effect is allowRecurse and is running', async () => { - const Child = defineComponent({ - props: ['getComputed', 'addComputedDep'], - created() { - this.addComputedDep() - }, - render: (props: { - getComputed: () => boolean - addComputedDep: () => void - }) => { - return h('div', props.getComputed()) - }, - }) - - const Parent = defineComponent(() => { - const count = ref(0) - const moreThanOne = computed(() => { - return count.value > 1 - }) - const getComputed = () => moreThanOne.value - const addComputedDep = () => { - count.value++ - } - return () => - h(Fragment, [ - getComputed() ? h('span', 'moreThanOne') : null, - h(Child, { getComputed, addComputedDep }), - h(Child, { getComputed, addComputedDep }), - h(Child, { getComputed, addComputedDep }), - ]) - }) - - const root = nodeOps.createElement('div') - render(h(Parent), root) - await nextTick() - expect(serialize(root)).toBe( - '
moreThanOne
true
true
true
', - ) - }) - describe('readonly + reactive for Map', () => { test('should work with readonly(reactive(Map))', () => { const m = reactive(new Map()) From 82046bdfa306a38e414b1ebab42029b3d801d208 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 12 Jan 2024 21:09:49 +0800 Subject: [PATCH 4/4] test: simplify and move test case --- .../reactivity/__tests__/computed.spec.ts | 48 ------------------- packages/reactivity/__tests__/effect.spec.ts | 32 +++++++++++++ 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 250768bdbc9..f66935d4c8d 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1,12 +1,3 @@ -import { - Fragment, - defineComponent, - h, - nextTick, - nodeOps, - render, - serialize, -} from '@vue/runtime-test' import { type DebuggerEvent, ITERATE_KEY, @@ -460,43 +451,4 @@ describe('reactivity/computed', () => { v.value = 2 expect(fnSpy).toBeCalledTimes(2) }) - it('should set dirtyLevel when effect is allowRecurse and is running', async () => { - const Child = defineComponent({ - props: ['getComputed', 'addComputedDep'], - created() { - this.addComputedDep() - }, - render: (props: { - getComputed: () => boolean - addComputedDep: () => void - }) => { - return h('div', props.getComputed()) - }, - }) - - const Parent = defineComponent(() => { - const count = ref(0) - const moreThanOne = computed(() => { - return count.value > 1 - }) - const getComputed = () => moreThanOne.value - const addComputedDep = () => { - count.value++ - } - return () => - h(Fragment, [ - getComputed() ? h('span', 'moreThanOne') : null, - h(Child, { getComputed, addComputedDep }), - h(Child, { getComputed, addComputedDep }), - h(Child, { getComputed, addComputedDep }), - ]) - }) - - const root = nodeOps.createElement('div') - render(h(Parent), root) - await nextTick() - expect(serialize(root)).toBe( - '
moreThanOne
true
true
true
', - ) - }) }) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 9109177bf49..925d9ed6104 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -3,16 +3,19 @@ import { type ReactiveEffectRunner, TrackOpTypes, TriggerOpTypes, + computed, effect, markRaw, reactive, readonly, + ref, shallowReactive, stop, toRaw, } from '../src/index' import { pauseScheduling, resetScheduling } from '../src/effect' import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' +import { h, nextTick, nodeOps, render, serialize } from '@vue/runtime-test' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -1011,6 +1014,35 @@ describe('reactivity/effect', () => { expect(counterSpy).toHaveBeenCalledTimes(1) }) + // #10082 + it('should set dirtyLevel when effect is allowRecurse and is running', async () => { + const s = ref(0) + const n = computed(() => s.value + 1) + + const Child = { + setup() { + s.value++ + return () => n.value + }, + } + + const renderSpy = vi.fn() + const Parent = { + setup() { + return () => { + renderSpy() + return [n.value, h(Child)] + } + }, + } + + const root = nodeOps.createElement('div') + render(h(Parent), root) + await nextTick() + expect(serialize(root)).toBe('
22
') + expect(renderSpy).toHaveBeenCalledTimes(2) + }) + describe('empty dep cleanup', () => { it('should remove the dep when the effect is stopped', () => { const obj = reactive({ prop: 1 })