Skip to content

Commit 8e9965d

Browse files
committed
Refactor InvokeGuardedCallbackImpl to not use metaprogramming
The implementation is largely unchanged except that the branching for when to use thebrowser specific override in dev is done in the function execution rather than by replacing the function implementation upon module evaluation.
1 parent fdad813 commit 8e9965d

File tree

1 file changed

+61
-78
lines changed

1 file changed

+61
-78
lines changed

packages/shared/invokeGuardedCallbackImpl.js

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,80 +7,46 @@
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);
3211
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
5412
if (
5513
typeof window !== 'undefined' &&
5614
typeof window.dispatchEvent === 'function' &&
5715
typeof document !== 'undefined' &&
5816
// $FlowFixMe[method-unbinding]
5917
typeof document.createEvent === 'function'
6018
) {
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-
}
19+
fakeNode = document.createElement('react');
20+
}
21+
}
8322

23+
export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
24+
this: {onError: (error: mixed) => void},
25+
name: string | null,
26+
func: (...Args) => mixed,
27+
context: Context,
28+
): void {
29+
if (__DEV__) {
30+
// In DEV mode, we use a special version
31+
// that plays more nicely with the browser's DevTools. The idea is to preserve
32+
// "Pause on exceptions" behavior. Because React wraps all user-provided
33+
// functions in invokeGuardedCallback, and the production version of
34+
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
35+
// like caught exceptions, and the DevTools won't pause unless the developer
36+
// takes the extra step of enabling pause on caught exceptions. This is
37+
// unintuitive, though, because even though React has caught the error, from
38+
// the developer's perspective, the error is uncaught.
39+
//
40+
// To preserve the expected "Pause on exceptions" behavior, we don't use a
41+
// try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
42+
// DOM node, and call the user-provided callback from inside an event handler
43+
// for that fake event. If the callback throws, the error is "captured" using
44+
// event loop context, it does not interrupt the normal program flow.
45+
// Effectively, this gives us try-catch behavior without actually using
46+
// try-catch. Neat!
47+
48+
// fakeNode signifies we are in an environment with a document and window object
49+
if (fakeNode) {
8450
const evt = document.createEvent('Event');
8551

8652
let didCall = false;
@@ -104,7 +70,7 @@ if (__DEV__) {
10470
'event',
10571
);
10672

107-
function restoreAfterDispatch() {
73+
const restoreAfterDispatch = () => {
10874
// We immediately remove the callback from event listeners so that
10975
// nested `invokeGuardedCallback` calls do not clash. Otherwise, a
11076
// nested call would trigger the fake event handlers of any call higher
@@ -121,20 +87,20 @@ if (__DEV__) {
12187
) {
12288
window.event = windowEvent;
12389
}
124-
}
90+
};
12591

12692
// Create an event handler for our fake event. We will synchronously
12793
// dispatch our fake event using `dispatchEvent`. Inside the handler, we
12894
// call the user-provided callback.
12995
// $FlowFixMe[method-unbinding]
13096
const funcArgs = Array.prototype.slice.call(arguments, 3);
131-
function callCallback() {
97+
const callCallback = () => {
13298
didCall = true;
13399
restoreAfterDispatch();
134100
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
135101
func.apply(context, funcArgs);
136102
didError = false;
137-
}
103+
};
138104

139105
// Create a global error event handler. We use this to capture the value
140106
// that was thrown. It's possible that this error handler will fire more
@@ -152,8 +118,7 @@ if (__DEV__) {
152118
let didSetError = false;
153119
let isCrossOriginError = false;
154120

155-
// $FlowFixMe[missing-local-annot]
156-
function handleWindowError(event) {
121+
const handleWindowError = (event: ErrorEvent) => {
157122
error = event.error;
158123
didSetError = true;
159124
if (error === null && event.colno === 0 && event.lineno === 0) {
@@ -171,7 +136,7 @@ if (__DEV__) {
171136
}
172137
}
173138
}
174-
}
139+
};
175140

176141
// Create a fake event type.
177142
const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
@@ -184,7 +149,6 @@ if (__DEV__) {
184149
// errors, it will trigger our global error handler.
185150
evt.initEvent(evtType, false, false);
186151
fakeNode.dispatchEvent(evt);
187-
188152
if (windowEventDescriptor) {
189153
Object.defineProperty(window, 'event', windowEventDescriptor);
190154
}
@@ -217,16 +181,35 @@ if (__DEV__) {
217181
// Remove our event listeners
218182
window.removeEventListener('error', handleWindowError);
219183

220-
if (!didCall) {
184+
if (didCall) {
185+
return;
186+
} else {
221187
// Something went really wrong, and our event was not dispatched.
222188
// https://github.com/facebook/react/issues/16734
223189
// https://github.com/facebook/react/issues/16585
224190
// Fall back to the production implementation.
225191
restoreAfterDispatch();
226-
return invokeGuardedCallbackProd.apply(this, arguments);
192+
// we fall through and call the prod version instead
227193
}
228-
};
194+
}
195+
// We only get here if we are in an environment that either does not support the browser
196+
// variant or we had trouble getting the browser to emit the error.
197+
// $FlowFixMe[method-unbinding]
198+
const funcArgs = Array.prototype.slice.call(arguments, 3);
199+
try {
200+
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
201+
func.apply(context, funcArgs);
202+
} catch (error) {
203+
this.onError(error);
204+
}
205+
} else {
206+
// $FlowFixMe[method-unbinding]
207+
const funcArgs = Array.prototype.slice.call(arguments, 3);
208+
try {
209+
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
210+
func.apply(context, funcArgs);
211+
} catch (error) {
212+
this.onError(error);
213+
}
229214
}
230215
}
231-
232-
export default invokeGuardedCallbackImpl;

0 commit comments

Comments
 (0)