invokeGuardedCallback
1 )概述
- 在 commit 阶段中在 DEV 环境中调用了
invokeGuardedCallback
这个方法 - 这个方法是在开发过程中被使用,用于捕获错误,协助开发调试的方法
2 )概述
定位到 packages/shared/ReactErrorUtils.js#L41
进入 invokeGuardedCallback
js
const reporter = {
onError(error: mixed) {
hasError = true;
caughtError = error;
},
};
/**
* Call a function while guarding against errors that happens within it.
* Returns an error if it throws, otherwise null.
*
* In production, this is implemented using a try-catch. The reason we don't
* use a try-catch directly is so that we can swap out a different
* implementation in DEV mode.
*
* @param {String} name of the guard to use for logging or debugging
* @param {Function} func The function to invoke
* @param {*} context The context to use when calling the function
* @param {...*} args Arguments for function
*/
export function invokeGuardedCallback<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
): void {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl.apply(reporter, arguments);
}
-
这个方法它实际调用了
invokeGuardedCallbackImpl.apply(reporter, arguments);
-
这个
reporter
,是定义的一个全局对象 -
内部初始化了两个变量
hasError
和caughtError
-
看下
invokeGuardedCallbackImpl
js// https://github.com/facebook/react/blob/v16.6.3/packages/shared/invokeGuardedCallbackImpl.js let invokeGuardedCallbackImpl = function<A, B, C, D, E, F, Context>( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed, context: Context, a: A, b: B, c: C, d: D, e: E, f: F, ) { const funcArgs = Array.prototype.slice.call(arguments, 3); try { func.apply(context, funcArgs); } catch (error) { this.onError(error); } }; 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 // untintuitive, 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 ( typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function' ) { const fakeNode = document.createElement('react'); const invokeGuardedCallbackDev = function<A, B, C, D, E, F, Context>( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed, context: Context, a: A, b: B, c: C, d: D, e: E, f: F, ) { // If document doesn't exist we know for sure we will crash in this method // when we call document.createEvent(). However this can cause confusing // errors: https://github.com/facebookincubator/create-react-app/issues/3482 // So we preemptively throw with a better message instead. // 首先会有一大串长的提醒,说明当前这个环境是不支持的 invariant( typeof document !== 'undefined', 'The `document` global was defined when React was initialized, but is not ' + 'defined anymore. This can happen in a test environment if a component ' + 'schedules an update from an asynchronous callback, but the test has already ' + 'finished running. To solve this, you can either unmount the component at ' + 'the end of your test (and ensure that any asynchronous operations get ' + 'canceled in `componentWillUnmount`), or you can change the test itself ' + 'to be asynchronous.', ); // 它创建了一个 event 对象 const evt = document.createEvent('Event'); // Keeps track of whether the user-provided callback threw an error. We // set this to true at the beginning, then set it to false right after // calling the function. If the function errors, `didError` will never be // set to false. This strategy works even if the browser is flaky and // fails to call our global error handler, because it doesn't rely on // the error event at all. // 声明了 didError 为true let didError = true; // Keeps track of the value of window.event so that we can reset it // during the callback to let user code access window.event in the // browsers that support it. let windowEvent = window.event; // Keeps track of the descriptor of window.event to restore it after event // dispatching: https://github.com/facebook/react/issues/13688 // 来获取 window.event 的对象描述符 const windowEventDescriptor = Object.getOwnPropertyDescriptor( window, 'event', ); // Create an event handler for our fake event. We will synchronously // dispatch our fake event using `dispatchEvent`. Inside the handler, we // call the user-provided callback. // 把前面3个参数给它拿出来,剩下的都是作为它的arguments const funcArgs = Array.prototype.slice.call(arguments, 3); // 它声明了一个叫做callcallback的一个方法。 function callCallback() { // We immediately remove the callback from event listeners so that // nested `invokeGuardedCallback` calls do not clash. Otherwise, a // nested call would trigger the fake event handlers of any call higher // in the stack. fakeNode.removeEventListener(evtType, callCallback, false); // 移除事件监听 // We check for window.hasOwnProperty('event') to prevent the // window.event assignment in both IE <= 10 as they throw an error // "Member not found" in strict mode, and in Firefox which does not // support window.event. // 对 window.event 进行兼容处理 if ( typeof window.event !== 'undefined' && window.hasOwnProperty('event') ) { window.event = windowEvent; } // 使用 context 调用 func 传入 funcArgs 后续的参数 // 注意,在 dev 时,这个 context 可能是 null, 可以向上追溯 func.apply(context, funcArgs); didError = false; } // 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 // than once; for example, if non-React code also calls `dispatchEvent` // and a handler for that event throws. We should be resilient to most of // those cases. Even if our error event handler fires more than once, the // last error event is always used. If the callback actually does error, // we know that the last error event is the correct one, because it's not // possible for anything else to have happened in between our callback // erroring and the code that follows the `dispatchEvent` call below. If // the callback doesn't error, but the error event was fired, we know to // ignore it because `didError` will be false, as described above. // 声明一堆和 error 相关的变量 let error; // Use this to track whether the error event is ever called. let didSetError = false; let isCrossOriginError = false; function handleWindowError(event) { error = event.error; didSetError = true; // 这种情况下是 跨域错误 if (error === null && event.colno === 0 && event.lineno === 0) { isCrossOriginError = true; } // 别的地方,有去处理 window.onerror,这里被阻塞了 if (event.defaultPrevented) { // Some other error handler has prevented default. // Browsers silence the error report if this happens. // We'll remember this to later decide whether to log it or not. if (error != null && typeof error === 'object') { try { error._suppressLogging = true; // 设置当前标识 } catch (inner) { // Ignore. } } } } // Create a fake event type. const evtType = `react-${name ? name : 'invokeguardedcallback'}`; // Attach our event handlers // 注册两个事件 window.addEventListener('error', handleWindowError); fakeNode.addEventListener(evtType, callCallback, false); // Synchronously dispatch our fake event. If the user-provided function // errors, it will trigger our global error handler. // 初始化事件,并立即触发 dispatchEvent evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt); // 触发完,会立即调用 callback if (windowEventDescriptor) { Object.defineProperty(window, 'event', windowEventDescriptor); } if (didError) { // 出错了,但是 handleWindowError 没有被调用的处理 if (!didSetError) { // The callback errored, but the error event never fired. error = new Error( 'An error was thrown inside one of your components, but React ' + "doesn't know what it was. This is likely due to browser " + 'flakiness. React does its best to preserve the "Pause on ' + 'exceptions" behavior of the DevTools, which requires some ' + "DEV-mode only tricks. It's possible that these don't work in " + 'your browser. Try triggering the error in production mode, ' + 'or switching to a modern browser. If you suspect that this is ' + 'actually an issue with React, please file an issue.', ); } else if (isCrossOriginError) { error = new Error( "A cross-origin error was thrown. React doesn't have access to " + 'the actual error object in development. ' + 'See https://fb.me/react-crossorigin-error for more information.', ); } this.onError(error); } // Remove our event listeners // 最终移除事件监听 window.removeEventListener('error', handleWindowError); }; invokeGuardedCallbackImpl = invokeGuardedCallbackDev; } }
- 它接收一系列的参数, 后续几个参数,它通过a,b,c,d,e,f来进行了一个命名
- 看着前期的定义好像很简单,但是下面有一个 DEV 的判断
- 这个方法 invokeGuardedCallbackDev 是最终 export 出去的
- 注意,在上述
handleWindowError
中,- 被触发相关事件时,就会执行这个回调函数
- 1 )当遇到跨域错误时,会做相关检测,并设置相关标识
- 2 )如果遇到 onError被阻塞,也是做一些标识处理
- 上述 evtType 的事件,一经调用,就会执行 上述的
callCallback
方法- callCallback 方法一旦被执行,就会执行参数中的
func
方法 - 当后续 func 执行中出现错误,
didError = false
就不会被执行 - 同时,出现错误,会调用
handleWindowError
这个方法,里面的变量也会发生变化
- callCallback 方法一旦被执行,就会执行参数中的
- 所以,里面的变量在整体流程中,非常有用
- 还有,就是 在最后的两个嵌套 if中
if (didError) { if (!didSetError) {} else if(isCrossOriginError) }
- 这时候会抛出错误, 分成两种情况,
- 参考:https://legacy.reactjs.org/docs/cross-origin-errors.html
- 为何不直接用try catch,而要用这种事件的方式来触发呢?
- 因为会存在这么一个问题,在浏览器的调试工具中
- 比方说,Sources面板,里面有个
Pause on exceptions
功能 - 如果有try catch会自行处理,但开发者本意不希望它停在那边,选中
Pause on caught exceptions
- 这样,即便通过trycatch捕获了这个错误,一旦代码报错了,我勾中了这个之后,代码也会因为报错而停止
- 开启该功能,使代码在捕获的错误发生的位置暂停,这样更方便定位问题
- 对于要去调试像 componentDidCatch 这种在组件内去处理错误的这种情况
- 它就会因为代码停在这里,而导致我们没有办法去调试这部分相关的功能
- 所以react为了防止这种情况的出现了,实现了这么一种方式