fix(scheduler): sort jobs before flushing
This fixes the case where a child component is added to the queue before its parent, but should be invalidated by its parent's update. Same logic was present in Vue 2. Properly fixes #910 ref: https://github.com/vuejs/vue-next/issues/910#issuecomment-613097539
This commit is contained in:
		
							parent
							
								
									c80b857eb5
								
							
						
					
					
						commit
						78977c3997
					
				| @ -12,6 +12,7 @@ const targetMap = new WeakMap<any, KeyToDepMap>() | |||||||
| export interface ReactiveEffect<T = any> { | export interface ReactiveEffect<T = any> { | ||||||
|   (...args: any[]): T |   (...args: any[]): T | ||||||
|   _isEffect: true |   _isEffect: true | ||||||
|  |   id: number | ||||||
|   active: boolean |   active: boolean | ||||||
|   raw: () => T |   raw: () => T | ||||||
|   deps: Array<Dep> |   deps: Array<Dep> | ||||||
| @ -21,7 +22,7 @@ export interface ReactiveEffect<T = any> { | |||||||
| export interface ReactiveEffectOptions { | export interface ReactiveEffectOptions { | ||||||
|   lazy?: boolean |   lazy?: boolean | ||||||
|   computed?: boolean |   computed?: boolean | ||||||
|   scheduler?: (job: () => void) => void |   scheduler?: (job: ReactiveEffect) => void | ||||||
|   onTrack?: (event: DebuggerEvent) => void |   onTrack?: (event: DebuggerEvent) => void | ||||||
|   onTrigger?: (event: DebuggerEvent) => void |   onTrigger?: (event: DebuggerEvent) => void | ||||||
|   onStop?: () => void |   onStop?: () => void | ||||||
| @ -74,6 +75,8 @@ export function stop(effect: ReactiveEffect) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | let uid = 0 | ||||||
|  | 
 | ||||||
| function createReactiveEffect<T = any>( | function createReactiveEffect<T = any>( | ||||||
|   fn: (...args: any[]) => T, |   fn: (...args: any[]) => T, | ||||||
|   options: ReactiveEffectOptions |   options: ReactiveEffectOptions | ||||||
| @ -96,6 +99,7 @@ function createReactiveEffect<T = any>( | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } as ReactiveEffect |   } as ReactiveEffect | ||||||
|  |   effect.id = uid++ | ||||||
|   effect._isEffect = true |   effect._isEffect = true | ||||||
|   effect.active = true |   effect.active = true | ||||||
|   effect.raw = fn |   effect.raw = fn | ||||||
|  | |||||||
| @ -262,4 +262,20 @@ describe('scheduler', () => { | |||||||
|     // job2 should be called only once
 |     // job2 should be called only once
 | ||||||
|     expect(calls).toEqual(['job1', 'job2', 'job3', 'job4']) |     expect(calls).toEqual(['job1', 'job2', 'job3', 'job4']) | ||||||
|   }) |   }) | ||||||
|  | 
 | ||||||
|  |   test('sort job based on id', async () => { | ||||||
|  |     const calls: string[] = [] | ||||||
|  |     const job1 = () => calls.push('job1') | ||||||
|  |     // job1 has no id
 | ||||||
|  |     const job2 = () => calls.push('job2') | ||||||
|  |     job2.id = 2 | ||||||
|  |     const job3 = () => calls.push('job3') | ||||||
|  |     job3.id = 1 | ||||||
|  | 
 | ||||||
|  |     queueJob(job1) | ||||||
|  |     queueJob(job2) | ||||||
|  |     queueJob(job3) | ||||||
|  |     await nextTick() | ||||||
|  |     expect(calls).toEqual(['job3', 'job2', 'job1']) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  | |||||||
| @ -1,7 +1,12 @@ | |||||||
| import { ErrorCodes, callWithErrorHandling } from './errorHandling' | import { ErrorCodes, callWithErrorHandling } from './errorHandling' | ||||||
| import { isArray } from '@vue/shared' | import { isArray } from '@vue/shared' | ||||||
| 
 | 
 | ||||||
| const queue: (Function | null)[] = [] | export interface Job { | ||||||
|  |   (): void | ||||||
|  |   id?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const queue: (Job | null)[] = [] | ||||||
| const postFlushCbs: Function[] = [] | const postFlushCbs: Function[] = [] | ||||||
| const p = Promise.resolve() | const p = Promise.resolve() | ||||||
| 
 | 
 | ||||||
| @ -9,20 +14,20 @@ let isFlushing = false | |||||||
| let isFlushPending = false | let isFlushPending = false | ||||||
| 
 | 
 | ||||||
| const RECURSION_LIMIT = 100 | const RECURSION_LIMIT = 100 | ||||||
| type CountMap = Map<Function, number> | type CountMap = Map<Job | Function, number> | ||||||
| 
 | 
 | ||||||
| export function nextTick(fn?: () => void): Promise<void> { | export function nextTick(fn?: () => void): Promise<void> { | ||||||
|   return fn ? p.then(fn) : p |   return fn ? p.then(fn) : p | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function queueJob(job: () => void) { | export function queueJob(job: Job) { | ||||||
|   if (!queue.includes(job)) { |   if (!queue.includes(job)) { | ||||||
|     queue.push(job) |     queue.push(job) | ||||||
|     queueFlush() |     queueFlush() | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function invalidateJob(job: () => void) { | export function invalidateJob(job: Job) { | ||||||
|   const i = queue.indexOf(job) |   const i = queue.indexOf(job) | ||||||
|   if (i > -1) { |   if (i > -1) { | ||||||
|     queue[i] = null |     queue[i] = null | ||||||
| @ -45,11 +50,9 @@ function queueFlush() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)] |  | ||||||
| 
 |  | ||||||
| export function flushPostFlushCbs(seen?: CountMap) { | export function flushPostFlushCbs(seen?: CountMap) { | ||||||
|   if (postFlushCbs.length) { |   if (postFlushCbs.length) { | ||||||
|     const cbs = dedupe(postFlushCbs) |     const cbs = [...new Set(postFlushCbs)] | ||||||
|     postFlushCbs.length = 0 |     postFlushCbs.length = 0 | ||||||
|     if (__DEV__) { |     if (__DEV__) { | ||||||
|       seen = seen || new Map() |       seen = seen || new Map() | ||||||
| @ -63,6 +66,8 @@ export function flushPostFlushCbs(seen?: CountMap) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const getId = (job: Job) => (job.id == null ? Infinity : job.id) | ||||||
|  | 
 | ||||||
| function flushJobs(seen?: CountMap) { | function flushJobs(seen?: CountMap) { | ||||||
|   isFlushPending = false |   isFlushPending = false | ||||||
|   isFlushing = true |   isFlushing = true | ||||||
| @ -70,6 +75,18 @@ function flushJobs(seen?: CountMap) { | |||||||
|   if (__DEV__) { |   if (__DEV__) { | ||||||
|     seen = seen || new Map() |     seen = seen || new Map() | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // Sort queue before flush.
 | ||||||
|  |   // This ensures that:
 | ||||||
|  |   // 1. Components are updated from parent to child. (because parent is always
 | ||||||
|  |   //    created before the child so its render effect will have smaller
 | ||||||
|  |   //    priority number)
 | ||||||
|  |   // 2. If a component is unmounted during a parent component's update,
 | ||||||
|  |   //    its update can be skipped.
 | ||||||
|  |   // Jobs can never be null before flush starts, since they are only invalidated
 | ||||||
|  |   // during execution of another flushed job.
 | ||||||
|  |   queue.sort((a, b) => getId(a!) - getId(b!)) | ||||||
|  | 
 | ||||||
|   while ((job = queue.shift()) !== undefined) { |   while ((job = queue.shift()) !== undefined) { | ||||||
|     if (job === null) { |     if (job === null) { | ||||||
|       continue |       continue | ||||||
| @ -88,7 +105,7 @@ function flushJobs(seen?: CountMap) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function checkRecursiveUpdates(seen: CountMap, fn: Function) { | function checkRecursiveUpdates(seen: CountMap, fn: Job | Function) { | ||||||
|   if (!seen.has(fn)) { |   if (!seen.has(fn)) { | ||||||
|     seen.set(fn, 1) |     seen.set(fn, 1) | ||||||
|   } else { |   } else { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user