[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.
This commit is contained in:
Josh Story 2023-04-20 15:08:51 -07:00 committed by GitHub
parent fdad813ac7
commit cc93a85332
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 60 additions and 77 deletions

View File

@ -7,50 +7,8 @@
* @flow * @flow
*/ */
// $FlowFixMe[missing-this-annot] let fakeNode: Element = (null: any);
function invokeGuardedCallbackProd<Args: Array<mixed>, Context>(
name: string | null,
func: (...Args) => mixed,
context: Context,
): void {
// $FlowFixMe[method-unbinding]
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}
let invokeGuardedCallbackImpl: <Args: Array<mixed>, Context>(
name: string | null,
func: (...Args) => mixed,
context: Context,
) => void = invokeGuardedCallbackProd;
if (__DEV__) { if (__DEV__) {
// In DEV mode, we swap out invokeGuardedCallback for a special version
// that plays more nicely with the browser's DevTools. The idea is to preserve
// "Pause on exceptions" behavior. Because React wraps all user-provided
// functions in invokeGuardedCallback, and the production version of
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
// like caught exceptions, and the DevTools won't pause unless the developer
// takes the extra step of enabling pause on caught exceptions. This is
// unintuitive, though, because even though React has caught the error, from
// the developer's perspective, the error is uncaught.
//
// To preserve the expected "Pause on exceptions" behavior, we don't use a
// try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
// DOM node, and call the user-provided callback from inside an event handler
// for that fake event. If the callback throws, the error is "captured" using
// a global event handler. But because the error happens in a different
// event loop context, it does not interrupt the normal program flow.
// Effectively, this gives us try-catch behavior without actually using
// try-catch. Neat!
// Check that the browser supports the APIs we need to implement our special
// DEV version of invokeGuardedCallback
if ( if (
typeof window !== 'undefined' && typeof window !== 'undefined' &&
typeof window.dispatchEvent === 'function' && typeof window.dispatchEvent === 'function' &&
@ -58,29 +16,37 @@ if (__DEV__) {
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
typeof document.createEvent === 'function' typeof document.createEvent === 'function'
) { ) {
const fakeNode = document.createElement('react'); fakeNode = document.createElement('react');
}
}
invokeGuardedCallbackImpl = function invokeGuardedCallbackDev< export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
Args: Array<mixed>, this: {onError: (error: mixed) => void},
Context, name: string | null,
// $FlowFixMe[missing-this-annot] func: (...Args) => mixed,
>(name: string | null, func: (...Args) => mixed, context: Context): void { context: Context,
// If document doesn't exist we know for sure we will crash in this method ): void {
// when we call document.createEvent(). However this can cause confusing if (__DEV__) {
// errors: https://github.com/facebook/create-react-app/issues/3482 // In DEV mode, we use a special version
// So we preemptively throw with a better message instead. // that plays more nicely with the browser's DevTools. The idea is to preserve
if (typeof document === 'undefined' || document === null) { // "Pause on exceptions" behavior. Because React wraps all user-provided
throw new Error( // functions in invokeGuardedCallback, and the production version of
'The `document` global was defined when React was initialized, but is not ' + // invokeGuardedCallback uses a try-catch, all user exceptions are treated
'defined anymore. This can happen in a test environment if a component ' + // like caught exceptions, and the DevTools won't pause unless the developer
'schedules an update from an asynchronous callback, but the test has already ' + // takes the extra step of enabling pause on caught exceptions. This is
'finished running. To solve this, you can either unmount the component at ' + // unintuitive, though, because even though React has caught the error, from
'the end of your test (and ensure that any asynchronous operations get ' + // the developer's perspective, the error is uncaught.
'canceled in `componentWillUnmount`), or you can change the test itself ' + //
'to be asynchronous.', // To preserve the expected "Pause on exceptions" behavior, we don't use a
); // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
} // DOM node, and call the user-provided callback from inside an event handler
// for that fake event. If the callback throws, the error is "captured" using
// event loop context, it does not interrupt the normal program flow.
// Effectively, this gives us try-catch behavior without actually using
// try-catch. Neat!
// fakeNode signifies we are in an environment with a document and window object
if (fakeNode) {
const evt = document.createEvent('Event'); const evt = document.createEvent('Event');
let didCall = false; let didCall = false;
@ -104,7 +70,7 @@ if (__DEV__) {
'event', 'event',
); );
function restoreAfterDispatch() { const restoreAfterDispatch = () => {
// We immediately remove the callback from event listeners so that // We immediately remove the callback from event listeners so that
// nested `invokeGuardedCallback` calls do not clash. Otherwise, a // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
// nested call would trigger the fake event handlers of any call higher // nested call would trigger the fake event handlers of any call higher
@ -121,20 +87,20 @@ if (__DEV__) {
) { ) {
window.event = windowEvent; window.event = windowEvent;
} }
} };
// Create an event handler for our fake event. We will synchronously // Create an event handler for our fake event. We will synchronously
// dispatch our fake event using `dispatchEvent`. Inside the handler, we // dispatch our fake event using `dispatchEvent`. Inside the handler, we
// call the user-provided callback. // call the user-provided callback.
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
const funcArgs = Array.prototype.slice.call(arguments, 3); const funcArgs = Array.prototype.slice.call(arguments, 3);
function callCallback() { const callCallback = () => {
didCall = true; didCall = true;
restoreAfterDispatch(); restoreAfterDispatch();
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing. // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
func.apply(context, funcArgs); func.apply(context, funcArgs);
didError = false; didError = false;
} };
// Create a global error event handler. We use this to capture the value // Create a global error event handler. We use this to capture the value
// that was thrown. It's possible that this error handler will fire more // that was thrown. It's possible that this error handler will fire more
@ -152,8 +118,7 @@ if (__DEV__) {
let didSetError = false; let didSetError = false;
let isCrossOriginError = false; let isCrossOriginError = false;
// $FlowFixMe[missing-local-annot] const handleWindowError = (event: ErrorEvent) => {
function handleWindowError(event) {
error = event.error; error = event.error;
didSetError = true; didSetError = true;
if (error === null && event.colno === 0 && event.lineno === 0) { if (error === null && event.colno === 0 && event.lineno === 0) {
@ -171,7 +136,7 @@ if (__DEV__) {
} }
} }
} }
} };
// Create a fake event type. // Create a fake event type.
const evtType = `react-${name ? name : 'invokeguardedcallback'}`; const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
@ -184,7 +149,6 @@ if (__DEV__) {
// errors, it will trigger our global error handler. // errors, it will trigger our global error handler.
evt.initEvent(evtType, false, false); evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt); fakeNode.dispatchEvent(evt);
if (windowEventDescriptor) { if (windowEventDescriptor) {
Object.defineProperty(window, 'event', windowEventDescriptor); Object.defineProperty(window, 'event', windowEventDescriptor);
} }
@ -217,16 +181,35 @@ if (__DEV__) {
// Remove our event listeners // Remove our event listeners
window.removeEventListener('error', handleWindowError); window.removeEventListener('error', handleWindowError);
if (!didCall) { if (didCall) {
return;
} else {
// Something went really wrong, and our event was not dispatched. // Something went really wrong, and our event was not dispatched.
// https://github.com/facebook/react/issues/16734 // https://github.com/facebook/react/issues/16734
// https://github.com/facebook/react/issues/16585 // https://github.com/facebook/react/issues/16585
// Fall back to the production implementation. // Fall back to the production implementation.
restoreAfterDispatch(); restoreAfterDispatch();
return invokeGuardedCallbackProd.apply(this, arguments); // we fall through and call the prod version instead
} }
}; }
// We only get here if we are in an environment that either does not support the browser
// variant or we had trouble getting the browser to emit the error.
// $FlowFixMe[method-unbinding]
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
} else {
// $FlowFixMe[method-unbinding]
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
// $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
} }
} }
export default invokeGuardedCallbackImpl;