7
7
* @flow
8
8
*/
9
9
10
- // $FlowFixMe[missing-this-annot]
11
- function invokeGuardedCallbackProd < Args : Array < mixed > , Context> (
12
- name : string | null ,
13
- func : ( ...Args ) => mixed ,
14
- context : Context ,
15
- ) : void {
16
- // $FlowFixMe[method-unbinding]
17
- const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
18
- try {
19
- // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
20
- func . apply ( context , funcArgs ) ;
21
- } catch ( error ) {
22
- this . onError ( error ) ;
23
- }
24
- }
25
-
26
- let invokeGuardedCallbackImpl : < Args : Array < mixed > , Context> (
27
- name : string | null ,
28
- func : ( ...Args ) => mixed ,
29
- context : Context ,
30
- ) => void = invokeGuardedCallbackProd ;
31
-
10
+ let fakeNode : Element = ( null : any ) ;
11
+ let doc : Document = ( null : any ) ;
12
+ let win : any = ( null : any ) ; // Window type
32
13
if ( __DEV__ ) {
33
- // In DEV mode, we swap out invokeGuardedCallback for a special version
34
- // that plays more nicely with the browser's DevTools. The idea is to preserve
35
- // "Pause on exceptions" behavior. Because React wraps all user-provided
36
- // functions in invokeGuardedCallback, and the production version of
37
- // invokeGuardedCallback uses a try-catch, all user exceptions are treated
38
- // like caught exceptions, and the DevTools won't pause unless the developer
39
- // takes the extra step of enabling pause on caught exceptions. This is
40
- // unintuitive, though, because even though React has caught the error, from
41
- // the developer's perspective, the error is uncaught.
42
- //
43
- // To preserve the expected "Pause on exceptions" behavior, we don't use a
44
- // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
45
- // DOM node, and call the user-provided callback from inside an event handler
46
- // for that fake event. If the callback throws, the error is "captured" using
47
- // a global event handler. But because the error happens in a different
48
- // event loop context, it does not interrupt the normal program flow.
49
- // Effectively, this gives us try-catch behavior without actually using
50
- // try-catch. Neat!
51
-
52
- // Check that the browser supports the APIs we need to implement our special
53
- // DEV version of invokeGuardedCallback
54
14
if (
55
15
typeof window !== 'undefined' &&
56
16
typeof window . dispatchEvent === 'function' &&
57
17
typeof document !== 'undefined' &&
58
18
// $FlowFixMe[method-unbinding]
59
19
typeof document . createEvent === 'function'
60
20
) {
61
- const fakeNode = document . createElement ( 'react' ) ;
62
-
63
- invokeGuardedCallbackImpl = function invokeGuardedCallbackDev <
64
- Args : Array < mixed > ,
65
- Context,
66
- // $FlowFixMe[missing-this-annot]
67
- > ( name : string | null , func : ( ...Args ) => mixed , context : Context ) : void {
68
- // If document doesn't exist we know for sure we will crash in this method
69
- // when we call document.createEvent(). However this can cause confusing
70
- // errors: https://github.com/facebook/create-react-app/issues/3482
71
- // So we preemptively throw with a better message instead.
72
- if ( typeof document === 'undefined' || document === null ) {
73
- throw new Error (
74
- 'The `document` global was defined when React was initialized, but is not ' +
75
- 'defined anymore. This can happen in a test environment if a component ' +
76
- 'schedules an update from an asynchronous callback, but the test has already ' +
77
- 'finished running. To solve this, you can either unmount the component at ' +
78
- 'the end of your test (and ensure that any asynchronous operations get ' +
79
- 'canceled in `componentWillUnmount`), or you can change the test itself ' +
80
- 'to be asynchronous.' ,
81
- ) ;
82
- }
21
+ fakeNode = document . createElement ( 'react' ) ;
22
+ doc = document ;
23
+ win = window ;
24
+ }
25
+ }
83
26
84
- const evt = document . createEvent ( 'Event ') ;
27
+ export default function invokeGuardedCallbackImpl < Args : Array < mixed > , Context> (
28
+ this : { onError : ( error : mixed ) => void } ,
29
+ name : string | null ,
30
+ func : ( ...Args ) => mixed ,
31
+ context : Context ,
32
+ ) : void {
33
+ if ( __DEV__ ) {
34
+ // In DEV mode, we use a special version
35
+ // that plays more nicely with the browser's DevTools. The idea is to preserve
36
+ // "Pause on exceptions" behavior. Because React wraps all user-provided
37
+ // functions in invokeGuardedCallback, and the production version of
38
+ // invokeGuardedCallback uses a try-catch, all user exceptions are treated
39
+ // like caught exceptions, and the DevTools won't pause unless the developer
40
+ // takes the extra step of enabling pause on caught exceptions. This is
41
+ // unintuitive, though, because even though React has caught the error, from
42
+ // the developer's perspective, the error is uncaught.
43
+ //
44
+ // To preserve the expected "Pause on exceptions" behavior, we don't use a
45
+ // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
46
+ // DOM node, and call the user-provided callback from inside an event handler
47
+ // for that fake event. If the callback throws, the error is "captured" using
48
+ // a global event handler. But because the error happens in a different
49
+ // event loop context, it does not interrupt the normal program flow.
50
+ // Effectively, this gives us try-catch behavior without actually using
51
+ // try-catch. Neat!
52
+
53
+ // fakeNode or doc or win could be our signal for whether we have set up the necessary
54
+ // state to execute this path. We just use fakeNode because we only need to check one of them.
55
+ if ( fakeNode ) {
56
+ const evt = doc . createEvent ( 'Event' ) ;
85
57
86
58
let didCall = false ;
87
59
// Keeps track of whether the user-provided callback threw an error. We
@@ -95,16 +67,16 @@ if (__DEV__) {
95
67
// Keeps track of the value of window.event so that we can reset it
96
68
// during the callback to let user code access window.event in the
97
69
// browsers that support it.
98
- const windowEvent = window . event ;
70
+ const windowEvent = win . event ;
99
71
100
72
// Keeps track of the descriptor of window.event to restore it after event
101
73
// dispatching: https://github.com/facebook/react/issues/13688
102
74
const windowEventDescriptor = Object . getOwnPropertyDescriptor (
103
- window ,
75
+ win ,
104
76
'event' ,
105
77
) ;
106
78
107
- function restoreAfterDispatch ( ) {
79
+ const restoreAfterDispatch = ( ) => {
108
80
// We immediately remove the callback from event listeners so that
109
81
// nested `invokeGuardedCallback` calls do not clash. Otherwise, a
110
82
// nested call would trigger the fake event handlers of any call higher
@@ -115,26 +87,23 @@ if (__DEV__) {
115
87
// window.event assignment in both IE <= 10 as they throw an error
116
88
// "Member not found" in strict mode, and in Firefox which does not
117
89
// support window.event.
118
- if (
119
- typeof window . event !== 'undefined' &&
120
- window . hasOwnProperty ( 'event' )
121
- ) {
122
- window . event = windowEvent ;
90
+ if ( typeof win . event !== 'undefined' && win . hasOwnProperty ( 'event' ) ) {
91
+ win . event = windowEvent ;
123
92
}
124
- }
93
+ } ;
125
94
126
95
// Create an event handler for our fake event. We will synchronously
127
96
// dispatch our fake event using `dispatchEvent`. Inside the handler, we
128
97
// call the user-provided callback.
129
98
// $FlowFixMe[method-unbinding]
130
99
const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
131
- function callCallback ( ) {
100
+ const callCallback = ( ) => {
132
101
didCall = true ;
133
102
restoreAfterDispatch ( ) ;
134
103
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
135
104
func . apply ( context , funcArgs ) ;
136
105
didError = false ;
137
- }
106
+ } ;
138
107
139
108
// Create a global error event handler. We use this to capture the value
140
109
// that was thrown. It's possible that this error handler will fire more
@@ -152,8 +121,7 @@ if (__DEV__) {
152
121
let didSetError = false ;
153
122
let isCrossOriginError = false ;
154
123
155
- // $FlowFixMe[missing-local-annot]
156
- function handleWindowError ( event ) {
124
+ const handleWindowError = ( event : ErrorEvent ) => {
157
125
error = event . error ;
158
126
didSetError = true ;
159
127
if ( error === null && event . colno === 0 && event . lineno === 0 ) {
@@ -171,22 +139,21 @@ if (__DEV__) {
171
139
}
172
140
}
173
141
}
174
- }
142
+ } ;
175
143
176
144
// Create a fake event type.
177
145
const evtType = `react-${ name ? name : 'invokeguardedcallback' } ` ;
178
146
179
147
// Attach our event handlers
180
- window . addEventListener ( 'error' , handleWindowError ) ;
148
+ win . addEventListener ( 'error' , handleWindowError ) ;
181
149
fakeNode . addEventListener ( evtType , callCallback , false ) ;
182
150
183
151
// Synchronously dispatch our fake event. If the user-provided function
184
152
// errors, it will trigger our global error handler.
185
153
evt . initEvent ( evtType , false , false ) ;
186
154
fakeNode . dispatchEvent ( evt ) ;
187
-
188
155
if ( windowEventDescriptor ) {
189
- Object . defineProperty ( window , 'event' , windowEventDescriptor ) ;
156
+ Object . defineProperty ( win , 'event' , windowEventDescriptor ) ;
190
157
}
191
158
192
159
if ( didCall && didError ) {
@@ -215,18 +182,37 @@ if (__DEV__) {
215
182
}
216
183
217
184
// Remove our event listeners
218
- window . removeEventListener ( 'error' , handleWindowError ) ;
185
+ win . removeEventListener ( 'error' , handleWindowError ) ;
219
186
220
- if ( ! didCall ) {
187
+ if ( didCall ) {
188
+ return ;
189
+ } else {
221
190
// Something went really wrong, and our event was not dispatched.
222
191
// https://github.com/facebook/react/issues/16734
223
192
// https://github.com/facebook/react/issues/16585
224
193
// Fall back to the production implementation.
225
194
restoreAfterDispatch ( ) ;
226
- return invokeGuardedCallbackProd . apply ( this , arguments ) ;
195
+ // we fall through and call the prod version instead
227
196
}
228
- } ;
197
+ }
198
+ // We only get here if we are in an environment that either does not support the browser
199
+ // variant or we had trouble getting the browser to emit the error.
200
+ // $FlowFixMe[method-unbinding]
201
+ const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
202
+ try {
203
+ // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
204
+ func . apply ( context , funcArgs ) ;
205
+ } catch ( error ) {
206
+ this . onError ( error ) ;
207
+ }
208
+ } else {
209
+ // $FlowFixMe[method-unbinding]
210
+ const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
211
+ try {
212
+ // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
213
+ func. apply ( context , funcArgs ) ;
214
+ } catch ( error ) {
215
+ this . onError ( error ) ;
216
+ }
229
217
}
230
218
}
231
-
232
- export default invokeGuardedCallbackImpl ;
0 commit comments