diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 495d6714939..2d2b4d2a1be 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -25,6 +25,7 @@ export interface ReactiveEffectOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void + allowRecurse?: boolean } export type DebuggerEvent = { @@ -178,7 +179,11 @@ export function trigger( const effects = new Set() const add = (effectsToAdd: Set | undefined) => { if (effectsToAdd) { - effectsToAdd.forEach(effect => effects.add(effect)) + effectsToAdd.forEach(effect => { + if (effect !== activeEffect || effect.options.allowRecurse) { + effects.add(effect) + } + }) } } diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 138a3e04f5d..7b64e50bd5f 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -779,4 +779,17 @@ describe('api: watch', () => { // should trigger now expect(sideEffect).toBe(2) }) + + // #2125 + test('watchEffect should not recursively trigger itself', async () => { + const spy = jest.fn() + const price = ref(10) + const history = ref([]) + watchEffect(() => { + history.value.push(price.value) + spy() + }) + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts index 243e4cbd0be..4253779d4cf 100644 --- a/packages/runtime-core/__tests__/rendererComponent.spec.ts +++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts @@ -5,7 +5,10 @@ import { nodeOps, serializeInner, nextTick, - VNode + VNode, + provide, + inject, + Ref } from '@vue/runtime-test' describe('renderer: component', () => { @@ -104,4 +107,34 @@ describe('renderer: component', () => { ) expect(Comp1.updated).not.toHaveBeenCalled() }) + + // #2043 + test('component child synchronously updating parent state should trigger parent re-render', async () => { + const App = { + setup() { + const n = ref(0) + provide('foo', n) + return () => { + return [h('div', n.value), h(Child)] + } + } + } + + const Child = { + setup() { + const n = inject>('foo')! + n.value++ + + return () => { + return h('div', n.value) + } + } + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe(`
0
1
`) + await nextTick() + expect(serializeInner(root)).toBe(`
1
1
`) + }) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index f38fc9da134..485dcf15077 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -44,7 +44,6 @@ import { flushPostFlushCbs, invalidateJob, flushPreFlushCbs, - SchedulerJob, SchedulerCb } from './scheduler' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' @@ -261,7 +260,9 @@ export const enum MoveType { } const prodEffectOptions = { - scheduler: queueJob + scheduler: queueJob, + // #1801, #2043 component render effects should allow recursive updates + allowRecurse: true } function createDevEffectOptions( @@ -269,6 +270,7 @@ function createDevEffectOptions( ): ReactiveEffectOptions { return { scheduler: queueJob, + allowRecurse: true, onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0, onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 } @@ -1489,8 +1491,6 @@ function baseCreateRenderer( } } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) - // #1801 mark it to allow recursive updates - ;(instance.update as SchedulerJob).allowRecurse = true } const updateComponentPreRender = (