1
1
import { ErrorCodes , callWithErrorHandling } from './errorHandling'
2
2
import { isArray } from '@vue/shared'
3
3
4
- export interface Job {
4
+ export interface SchedulerJob {
5
5
( ) : void
6
+ /**
7
+ * unique job id, only present on raw effects, e.g. component render effect
8
+ */
6
9
id ?: number
10
+ /**
11
+ * Indicates this is a watch() callback and is allowed to trigger itself.
12
+ * A watch callback doesn't track its dependencies so if it triggers itself
13
+ * again, it's likely intentional and it is the user's responsibility to
14
+ * perform recursive state mutation that eventually stabilizes.
15
+ */
16
+ cb ?: boolean
7
17
}
8
18
9
- const queue : ( Job | null ) [ ] = [ ]
19
+ const queue : ( SchedulerJob | null ) [ ] = [ ]
10
20
const postFlushCbs : Function [ ] = [ ]
11
21
const resolvedPromise : Promise < any > = Promise . resolve ( )
12
22
let currentFlushPromise : Promise < void > | null = null
13
23
14
24
let isFlushing = false
15
25
let isFlushPending = false
16
- let flushIndex = - 1
26
+ let flushIndex = 0
17
27
let pendingPostFlushCbs : Function [ ] | null = null
18
28
let pendingPostFlushIndex = 0
19
29
20
30
const RECURSION_LIMIT = 100
21
- type CountMap = Map < Job | Function , number >
31
+ type CountMap = Map < SchedulerJob | Function , number >
22
32
23
33
export function nextTick ( fn ?: ( ) => void ) : Promise < void > {
24
34
const p = currentFlushPromise || resolvedPromise
25
35
return fn ? p . then ( fn ) : p
26
36
}
27
37
28
- export function queueJob ( job : Job ) {
29
- if ( ! queue . includes ( job , flushIndex + 1 ) ) {
38
+ export function queueJob ( job : SchedulerJob ) {
39
+ // the dedupe search uses the startIndex argument of Array.includes()
40
+ // by default the search index includes the current job that is being run
41
+ // so it cannot recursively trigger itself again.
42
+ // if the job is a watch() callback, the search will start with a +1 index to
43
+ // allow it recursively trigger itself - it is the user's responsibility to
44
+ // ensure it doesn't end up in an infinite loop.
45
+ if (
46
+ ! queue . length ||
47
+ ! queue . includes ( job , job . cb ? flushIndex + 1 : flushIndex )
48
+ ) {
30
49
queue . push ( job )
31
50
queueFlush ( )
32
51
}
33
52
}
34
53
35
- export function invalidateJob ( job : Job ) {
54
+ export function invalidateJob ( job : SchedulerJob ) {
36
55
const i = queue . indexOf ( job )
37
56
if ( i > - 1 ) {
38
57
queue [ i ] = null
@@ -43,7 +62,12 @@ export function queuePostFlushCb(cb: Function | Function[]) {
43
62
if ( ! isArray ( cb ) ) {
44
63
if (
45
64
! pendingPostFlushCbs ||
46
- ! pendingPostFlushCbs . includes ( cb , pendingPostFlushIndex + 1 )
65
+ ! pendingPostFlushCbs . includes (
66
+ cb ,
67
+ ( cb as SchedulerJob ) . cb
68
+ ? pendingPostFlushIndex + 1
69
+ : pendingPostFlushIndex
70
+ )
47
71
) {
48
72
postFlushCbs . push ( cb )
49
73
}
@@ -85,7 +109,7 @@ export function flushPostFlushCbs(seen?: CountMap) {
85
109
}
86
110
}
87
111
88
- const getId = ( job : Job ) => ( job . id == null ? Infinity : job . id )
112
+ const getId = ( job : SchedulerJob ) => ( job . id == null ? Infinity : job . id )
89
113
90
114
function flushJobs ( seen ?: CountMap ) {
91
115
isFlushPending = false
@@ -105,38 +129,43 @@ function flushJobs(seen?: CountMap) {
105
129
// during execution of another flushed job.
106
130
queue . sort ( ( a , b ) => getId ( a ! ) - getId ( b ! ) )
107
131
108
- for ( flushIndex = 0 ; flushIndex < queue . length ; flushIndex ++ ) {
109
- const job = queue [ flushIndex ]
110
- if ( job ) {
111
- if ( __DEV__ ) {
112
- checkRecursiveUpdates ( seen ! , job )
132
+ try {
133
+ for ( flushIndex = 0 ; flushIndex < queue . length ; flushIndex ++ ) {
134
+ const job = queue [ flushIndex ]
135
+ if ( job ) {
136
+ if ( __DEV__ ) {
137
+ checkRecursiveUpdates ( seen ! , job )
138
+ }
139
+ callWithErrorHandling ( job , null , ErrorCodes . SCHEDULER )
113
140
}
114
- callWithErrorHandling ( job , null , ErrorCodes . SCHEDULER )
115
141
}
116
- }
117
- flushIndex = - 1
118
- queue . length = 0
119
-
120
- flushPostFlushCbs ( seen )
121
- isFlushing = false
122
- currentFlushPromise = null
123
- // some postFlushCb queued jobs!
124
- // keep flushing until it drains.
125
- if ( queue . length || postFlushCbs . length ) {
126
- flushJobs ( seen )
142
+ } finally {
143
+ flushIndex = 0
144
+ queue . length = 0
145
+
146
+ flushPostFlushCbs ( seen )
147
+ isFlushing = false
148
+ currentFlushPromise = null
149
+ // some postFlushCb queued jobs!
150
+ // keep flushing until it drains.
151
+ if ( queue . length || postFlushCbs . length ) {
152
+ flushJobs ( seen )
153
+ }
127
154
}
128
155
}
129
156
130
- function checkRecursiveUpdates ( seen : CountMap , fn : Job | Function ) {
157
+ function checkRecursiveUpdates ( seen : CountMap , fn : SchedulerJob | Function ) {
131
158
if ( ! seen . has ( fn ) ) {
132
159
seen . set ( fn , 1 )
133
160
} else {
134
161
const count = seen . get ( fn ) !
135
162
if ( count > RECURSION_LIMIT ) {
136
163
throw new Error (
137
- 'Maximum recursive updates exceeded. ' +
138
- "You may have code that is mutating state in your component's " +
139
- 'render function or updated hook or watcher source function.'
164
+ `Maximum recursive updates exceeded. ` +
165
+ `This means you have a reactive effect that is mutating its own ` +
166
+ `dependencies and thus recursively triggering itself. Possible sources ` +
167
+ `include component template, render function, updated hook or ` +
168
+ `watcher source function.`
140
169
)
141
170
} else {
142
171
seen . set ( fn , count + 1 )
0 commit comments