fix(runtime-core/scheduler): allow component render functions to trigger itself

fix #1801
This commit is contained in:
Evan You 2020-08-13 17:27:14 -04:00
parent bc6f252c4a
commit 611437a3fe
4 changed files with 24 additions and 12 deletions

View File

@ -451,7 +451,7 @@ describe('scheduler', () => {
expect(count).toBe(1) 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 // normal job
let count = 0 let count = 0
const job = () => { const job = () => {
@ -460,7 +460,7 @@ describe('scheduler', () => {
queueJob(job) queueJob(job)
} }
} }
job.cb = true job.allowRecurse = true
queueJob(job) queueJob(job)
await nextTick() await nextTick()
expect(count).toBe(3) expect(count).toBe(3)
@ -472,7 +472,7 @@ describe('scheduler', () => {
queuePostFlushCb(cb) queuePostFlushCb(cb)
} }
} }
cb.cb = true cb.allowRecurse = true
queuePostFlushCb(cb) queuePostFlushCb(cb)
await nextTick() await nextTick()
expect(count).toBe(5) expect(count).toBe(5)

View File

@ -261,7 +261,7 @@ function doWatch(
// important: mark the job as a watcher callback so that scheduler knows it // important: mark the job as a watcher callback so that scheduler knows it
// it is allowed to self-trigger (#1727) // it is allowed to self-trigger (#1727)
job.cb = !!cb job.allowRecurse = !!cb
let scheduler: (job: () => any) => void let scheduler: (job: () => any) => void
if (flush === 'sync') { if (flush === 'sync') {

View File

@ -41,7 +41,8 @@ import {
queuePostFlushCb, queuePostFlushCb,
flushPostFlushCbs, flushPostFlushCbs,
invalidateJob, invalidateJob,
flushPreFlushCbs flushPreFlushCbs,
SchedulerJob
} from './scheduler' } from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
import { updateProps } from './componentProps' import { updateProps } from './componentProps'
@ -1429,6 +1430,8 @@ 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 = (

View File

@ -8,12 +8,18 @@ export interface SchedulerJob {
*/ */
id?: number id?: number
/** /**
* Indicates this is a watch() callback and is allowed to trigger itself. * Indicates whether the job is allowed to recursively trigger itself.
* A watch callback doesn't track its dependencies so if it triggers itself * By default, a job cannot trigger itself because some built-in method calls,
* again, it's likely intentional and it is the user's responsibility to * e.g. Array.prototype.push actually performs reads as well (#1740) which
* perform recursive state mutation that eventually stabilizes. * 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 let isFlushing = false
@ -54,7 +60,7 @@ export function queueJob(job: SchedulerJob) {
(!queue.length || (!queue.length ||
!queue.includes( !queue.includes(
job, job,
isFlushing && job.cb ? flushIndex + 1 : flushIndex isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) && )) &&
job !== currentPreFlushParentJob job !== currentPreFlushParentJob
) { ) {
@ -86,7 +92,10 @@ function queueCb(
if (!isArray(cb)) { if (!isArray(cb)) {
if ( if (
!activeQueue || !activeQueue ||
!activeQueue.includes(cb, (cb as SchedulerJob).cb ? index + 1 : index) !activeQueue.includes(
cb,
(cb as SchedulerJob).allowRecurse ? index + 1 : index
)
) { ) {
pendingQueue.push(cb) pendingQueue.push(cb)
} }