expect(count).toBe(1)
})
- test('should allow watcher callbacks to trigger itself', async () => {
+ test('should allow explicitly marked jobs to trigger itself', async () => {
// normal job
let count = 0
const job = () => {
queueJob(job)
}
}
- job.cb = true
+ job.allowRecurse = true
queueJob(job)
await nextTick()
expect(count).toBe(3)
queuePostFlushCb(cb)
}
}
- cb.cb = true
+ cb.allowRecurse = true
queuePostFlushCb(cb)
await nextTick()
expect(count).toBe(5)
queuePostFlushCb,
flushPostFlushCbs,
invalidateJob,
- flushPreFlushCbs
+ flushPreFlushCbs,
+ SchedulerJob
} from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
import { updateProps } from './componentProps'
}
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
+ // #1801 mark it to allow recursive updates
+ ;(instance.update as SchedulerJob).allowRecurse = true
}
const updateComponentPreRender = (
*/
id?: number
/**
- * Indicates this is a watch() callback and is allowed to trigger itself.
- * A watch callback 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.
+ * Indicates whether the job is allowed to recursively trigger itself.
+ * 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 render functions and watch callbacks.
+ * Render 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).
*/
- cb?: boolean
+ allowRecurse?: boolean
}
let isFlushing = false
(!queue.length ||
!queue.includes(
job,
- isFlushing && job.cb ? flushIndex + 1 : flushIndex
+ isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (!isArray(cb)) {
if (
!activeQueue ||
- !activeQueue.includes(cb, (cb as SchedulerJob).cb ? index + 1 : index)
+ !activeQueue.includes(
+ cb,
+ (cb as SchedulerJob).allowRecurse ? index + 1 : index
+ )
) {
pendingQueue.push(cb)
}