@samitouri / QOS-React-2 / commits / cc93a85332

[Fiber] InvokeGuardedCallback without metaprogramming (#26569)

InvokeGuardedCallback is now implemented with the browser fork done at error-time rather than module-load-time. Originally it also tried to freeze the window/document references to avoid mismatches in prototype chains when testing React in different documents however we have since updated our tests to not do this and it was a test only feature so I removed it.

Josh Story committed Apr 20, 2023 at 15:08 UTC cc93a8533264618b7a8e3d6fb56df917214c19d7
1 file changed +61 -78
packages/shared/invokeGuardedCallbackImpl.js
+61 -78
@@ -7,50 +7,8 @@
7 * @flow
8 */
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 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
12 if (
13 typeof window !== 'undefined' &&
14 typeof window.dispatchEvent === 'function' &&
@@ -58,29 +16,37 @@ if (__DEV__) {
16 // $FlowFixMe[method-unbinding]
17 typeof document.createEvent === 'function'
18 ) {
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 +}
22
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) {
50 const evt = document.createEvent('Event');
51
52 let didCall = false;
@@ -104,7 +70,7 @@ if (__DEV__) {
70 'event',
71 );
72
107 - function restoreAfterDispatch() {
73 + const restoreAfterDispatch = () => {
74 // We immediately remove the callback from event listeners so that
75 // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
76 // nested call would trigger the fake event handlers of any call higher
@@ -121,20 +87,20 @@ if (__DEV__) {
87 ) {
88 window.event = windowEvent;
89 }
124 - }
90 + };
91
92 // Create an event handler for our fake event. We will synchronously
93 // dispatch our fake event using `dispatchEvent`. Inside the handler, we
94 // call the user-provided callback.
95 // $FlowFixMe[method-unbinding]
96 const funcArgs = Array.prototype.slice.call(arguments, 3);
131 - function callCallback() {
97 + const callCallback = () => {
98 didCall = true;
99 restoreAfterDispatch();
100 // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
101 func.apply(context, funcArgs);
102 didError = false;
137 - }
103 + };
104
105 // Create a global error event handler. We use this to capture the value
106 // that was thrown. It's possible that this error handler will fire more
@@ -152,8 +118,7 @@ if (__DEV__) {
118 let didSetError = false;
119 let isCrossOriginError = false;
120
155 - // $FlowFixMe[missing-local-annot]
156 - function handleWindowError(event) {
121 + const handleWindowError = (event: ErrorEvent) => {
122 error = event.error;
123 didSetError = true;
124 if (error === null && event.colno === 0 && event.lineno === 0) {
@@ -171,7 +136,7 @@ if (__DEV__) {
136 }
137 }
138 }
174 - }
139 + };
140
141 // Create a fake event type.
142 const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
@@ -184,7 +149,6 @@ if (__DEV__) {
149 // errors, it will trigger our global error handler.
150 evt.initEvent(evtType, false, false);
151 fakeNode.dispatchEvent(evt);
187 -
152 if (windowEventDescriptor) {
153 Object.defineProperty(window, 'event', windowEventDescriptor);
154 }
@@ -217,16 +181,35 @@ if (__DEV__) {
181 // Remove our event listeners
182 window.removeEventListener('error', handleWindowError);
183
220 - if (!didCall) {
184 + if (didCall) {
185 + return;
186 + } else {
187 // Something went really wrong, and our event was not dispatched.
188 // https://github.com/facebook/react/issues/16734
189 // https://github.com/facebook/react/issues/16585
190 // Fall back to the production implementation.
191 restoreAfterDispatch();
226 - return invokeGuardedCallbackProd.apply(this, arguments);
192 + // we fall through and call the prod version instead
193 }
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 + }
214 }
215 }
231 -
232 -export default invokeGuardedCallbackImpl;