fix(reactivity): effect shoud only recursively self trigger with explicit options

fix #2125
This commit is contained in:
Evan You 2020-09-16 10:52:31 -04:00
parent 89e9ab8a2a
commit 3810de7d6b
4 changed files with 57 additions and 6 deletions

View File

@ -25,6 +25,7 @@ export interface ReactiveEffectOptions {
onTrack?: (event: DebuggerEvent) => void onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void
onStop?: () => void onStop?: () => void
allowRecurse?: boolean
} }
export type DebuggerEvent = { export type DebuggerEvent = {
@ -178,7 +179,11 @@ export function trigger(
const effects = new Set<ReactiveEffect>() const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) { if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect)) effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.options.allowRecurse) {
effects.add(effect)
}
})
} }
} }

View File

@ -779,4 +779,17 @@ describe('api: watch', () => {
// should trigger now // should trigger now
expect(sideEffect).toBe(2) expect(sideEffect).toBe(2)
}) })
// #2125
test('watchEffect should not recursively trigger itself', async () => {
const spy = jest.fn()
const price = ref(10)
const history = ref<number[]>([])
watchEffect(() => {
history.value.push(price.value)
spy()
})
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -5,7 +5,10 @@ import {
nodeOps, nodeOps,
serializeInner, serializeInner,
nextTick, nextTick,
VNode VNode,
provide,
inject,
Ref
} from '@vue/runtime-test' } from '@vue/runtime-test'
describe('renderer: component', () => { describe('renderer: component', () => {
@ -104,4 +107,34 @@ describe('renderer: component', () => {
) )
expect(Comp1.updated).not.toHaveBeenCalled() 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<Ref<number>>('foo')!
n.value++
return () => {
return h('div', n.value)
}
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div>0</div><div>1</div>`)
await nextTick()
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
})
}) })

View File

@ -44,7 +44,6 @@ import {
flushPostFlushCbs, flushPostFlushCbs,
invalidateJob, invalidateJob,
flushPreFlushCbs, flushPreFlushCbs,
SchedulerJob,
SchedulerCb SchedulerCb
} from './scheduler' } from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
@ -261,7 +260,9 @@ export const enum MoveType {
} }
const prodEffectOptions = { const prodEffectOptions = {
scheduler: queueJob scheduler: queueJob,
// #1801, #2043 component render effects should allow recursive updates
allowRecurse: true
} }
function createDevEffectOptions( function createDevEffectOptions(
@ -269,6 +270,7 @@ function createDevEffectOptions(
): ReactiveEffectOptions { ): ReactiveEffectOptions {
return { return {
scheduler: queueJob, scheduler: queueJob,
allowRecurse: true,
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0, onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
} }
@ -1489,8 +1491,6 @@ function baseCreateRenderer(
} }
} }
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
// #1801 mark it to allow recursive updates
;(instance.update as SchedulerJob).allowRecurse = true
} }
const updateComponentPreRender = ( const updateComponentPreRender = (