77 * @flow
88 */
99
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
3213if ( __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
5414 if (
5515 typeof window !== 'undefined' &&
5616 typeof window . dispatchEvent === 'function' &&
5717 typeof document !== 'undefined' &&
5818 // $FlowFixMe[method-unbinding]
5919 typeof document . createEvent === 'function'
6020 ) {
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+ }
8326
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' ) ;
8557
8658 let didCall = false ;
8759 // Keeps track of whether the user-provided callback threw an error. We
@@ -95,16 +67,16 @@ if (__DEV__) {
9567 // Keeps track of the value of window.event so that we can reset it
9668 // during the callback to let user code access window.event in the
9769 // browsers that support it.
98- const windowEvent = window . event ;
70+ const windowEvent = win . event ;
9971
10072 // Keeps track of the descriptor of window.event to restore it after event
10173 // dispatching: https://github.com/facebook/react/issues/13688
10274 const windowEventDescriptor = Object . getOwnPropertyDescriptor (
103- window ,
75+ win ,
10476 'event' ,
10577 ) ;
10678
107- function restoreAfterDispatch ( ) {
79+ const restoreAfterDispatch = ( ) => {
10880 // We immediately remove the callback from event listeners so that
10981 // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
11082 // nested call would trigger the fake event handlers of any call higher
@@ -115,26 +87,23 @@ if (__DEV__) {
11587 // window.event assignment in both IE <= 10 as they throw an error
11688 // "Member not found" in strict mode, and in Firefox which does not
11789 // 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 ;
12392 }
124- }
93+ } ;
12594
12695 // Create an event handler for our fake event. We will synchronously
12796 // dispatch our fake event using `dispatchEvent`. Inside the handler, we
12897 // call the user-provided callback.
12998 // $FlowFixMe[method-unbinding]
13099 const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
131- function callCallback ( ) {
100+ const callCallback = ( ) => {
132101 didCall = true ;
133102 restoreAfterDispatch ( ) ;
134103 // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
135104 func . apply ( context , funcArgs ) ;
136105 didError = false ;
137- }
106+ } ;
138107
139108 // Create a global error event handler. We use this to capture the value
140109 // that was thrown. It's possible that this error handler will fire more
@@ -152,8 +121,7 @@ if (__DEV__) {
152121 let didSetError = false ;
153122 let isCrossOriginError = false ;
154123
155- // $FlowFixMe[missing-local-annot]
156- function handleWindowError ( event ) {
124+ const handleWindowError = ( event : ErrorEvent ) => {
157125 error = event . error ;
158126 didSetError = true ;
159127 if ( error === null && event . colno === 0 && event . lineno === 0 ) {
@@ -171,22 +139,21 @@ if (__DEV__) {
171139 }
172140 }
173141 }
174- }
142+ } ;
175143
176144 // Create a fake event type.
177145 const evtType = `react-${ name ? name : 'invokeguardedcallback' } ` ;
178146
179147 // Attach our event handlers
180- window . addEventListener ( 'error' , handleWindowError ) ;
148+ win . addEventListener ( 'error' , handleWindowError ) ;
181149 fakeNode . addEventListener ( evtType , callCallback , false ) ;
182150
183151 // Synchronously dispatch our fake event. If the user-provided function
184152 // errors, it will trigger our global error handler.
185153 evt . initEvent ( evtType , false , false ) ;
186154 fakeNode . dispatchEvent ( evt ) ;
187-
188155 if ( windowEventDescriptor ) {
189- Object . defineProperty ( window , 'event' , windowEventDescriptor ) ;
156+ Object . defineProperty ( win , 'event' , windowEventDescriptor ) ;
190157 }
191158
192159 if ( didCall && didError ) {
@@ -215,18 +182,37 @@ if (__DEV__) {
215182 }
216183
217184 // Remove our event listeners
218- window . removeEventListener ( 'error' , handleWindowError ) ;
185+ win . removeEventListener ( 'error' , handleWindowError ) ;
219186
220- if ( ! didCall ) {
187+ if ( didCall ) {
188+ return ;
189+ } else {
221190 // Something went really wrong, and our event was not dispatched.
222191 // https://github.com/facebook/react/issues/16734
223192 // https://github.com/facebook/react/issues/16585
224193 // Fall back to the production implementation.
225194 restoreAfterDispatch ( ) ;
226- return invokeGuardedCallbackProd . apply ( this , arguments ) ;
195+ // we fall through and call the prod version instead
227196 }
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+ }
229217 }
230218}
231-
232- export default invokeGuardedCallbackImpl ;
0 commit comments