[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:
parent
fdad813ac7
commit
cc93a85332
|
@ -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;
|
|
||||||
|
|
Loading…
Reference in New Issue