持续更新中react相关库源码浅析
解决的两个问题
- 收集错误
- 自定义事件实现
try...catch
错误捕获功能同时增加Pause on exceptions
功能
react\packages\shared\ReactErrorUtils.js
用于存储错误以及是否发生错误的状态
// Used by Fiber to simulate a try-catch.
let hasError: boolean = false;
let caughtError: mixed = null;
需要重新抛出的错误以及重新抛出的状态
// Used by event system to capture/rethrow the first error.
let hasRethrowError: boolean = false;
let rethrowError: mixed = null;
reporter
对象上的onError
方法提供一个借口改变错误的状态以及存储捕获到的错误对象
const reporter = {
onError(error: mixed) {
hasError = true;
caughtError = error;
},
};
错误系统入口函数,用于捕获第一个错误
对invokeGuardedCallback
的封装,当执行完invokeGuardedCallback
之后,通过hasError
判断是否发生了错误,并调用clearCaughtError
返回错误对象,并清空hasError、caughtError
。然后将返回的错误存储到rethrowError
,并改变hasRethrowError
的状态。
function invokeGuardedCallbackAndCatchFirstError(name,func,context,a,b,c,d,e,f) {
invokeGuardedCallback.apply(this, arguments);
if (hasError) {
const error = clearCaughtError();
if (!hasRethrowError) {
hasRethrowError = true;
rethrowError = error;
}
}
}
export function clearCaughtError() {
if (hasError) {
const error = caughtError;
hasError = false;
caughtError = null;
return error;
} else {
invariant(
false,
'clearCaughtError was called but no error was captured. This error ' +
'is likely caused by a bug in React. Please file an issue.',
);
}
}
调用传入函数func
的准备
对invokeGuardedCallbackImpl
的封装,利用apply
将this
指向reporter
,用于当invokeGuardedCallbackImpl
函数发生错误的时候,调用reporter
上的接口onError
,将错误对象存储到最外层ReactErrorUtils.js
文件模块的hasError、caughtError
变量上。
export function invokeGuardedCallback(name,func,context,a,b,c,d,e,f) {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl.apply(reporter, arguments);
}
直接执行的func
的invokeGuardedCallbackImpl
invokeGuardedCallbackImpl
在不同的环境下有不同的实现:
- 在生产环境下利用
try ... catch
捕获错误,并存储错误到hasError、caughtError
上。 - 在开发环境下利用自定义事件模拟了
try ... catch
功能,同时具备在开发工具中在所有异常发生的地方自动断点的功能。如果用try ... catch
,那么try
块语句中出现的错误不会断点暂停,因为这个异常被catch
捕获了,除非在chrome下将Pause on exceptions
打开。
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.
下面主要分析invokeGuardedCallbackImpl
react\packages\shared\invokeGuardedCallbackImpl.js
生产环境:利用try-catch实现错误捕获
let invokeGuardedCallbackImpl = function(name,func,context,a,b,c,d,e,f){
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
};
将arguments
转成数组,然后执行传入的函数func
,执行上下文指定为传入的context
。如果func
执行过程中发生错误,抛出的错误被捕获并通过this.onError
传入最外层的变量上。这里的this
指向的是reporter
。
开发环境:模拟try-catch
const fakeNode = document.createElement('react');
const invokeGuardedCallbackDev = function(name,func,context,a,b,c,d,e,f){
let windowEvent = window.event;
const windowEventDescriptor = Object.getOwnPropertyDescriptor(
window,
'event',
);
const funcArgs = Array.prototype.slice.call(arguments, 3);
/**
* 给window添加error事件监听函数,未捕获的错误都会经过这里
*/
let error;
let didSetError = false;
let isCrossOriginError = false;
function handleWindowError(event) {
error = event.error;
didSetError = true;
if (error === null && event.colno === 0 && event.lineno === 0) {
// 当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而代之简单的"Script error."
// event:
// message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
isCrossOriginError = true;
}
if (event.defaultPrevented) {
if (error != null && typeof error === 'object') {
try {
error._suppressLogging = true;
} catch (inner) {
// Ignore.
}
}
}
}
window.addEventListener('error', handleWindowError);
/**
* 利用createEvent创建自定义事件event,并利用initEvent指定名称,然后给DOM fakeNode添加evtType事件监听函数
*/
let didError = true;
const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
const evt = document.createEvent('Event');
evt.initEvent(evtType, false/*false表示阻止该事件向上冒泡*/, false/*false表示该事件默认动作不可取消*/);
function callCallback() {
fakeNode.removeEventListener(evtType, callCallback, false);
if (
typeof window.event !== 'undefined' &&
window.hasOwnProperty('event')
) {
window.event = windowEvent;
}
func.apply(context, funcArgs);
didError = false;
}
fakeNode.addEventListener(evtType, callCallback, false);
/**
* 手动触发fakeNode上的evt事件,此时会先执行传入的func函数,如果发生错误会触发window上的error事件
*/
fakeNode.dispatchEvent(evt);
/**
* 还原window.event
*/
if (windowEventDescriptor) {
Object.defineProperty(window, 'event', windowEventDescriptor);
}
/**
* 调用onError记录error
*/
if (didError) {
if (!didSetError) {
error = new Error(
'An error was thrown inside one of your components'
);
} else if (isCrossOriginError) {
error = new Error(
"A cross-origin error was thrown. React doesn't have access to ",
);
}
this.onError(error);
}
// Remove our event listeners
window.removeEventListener('error', handleWindowError);
};
invokeGuardedCallbackImpl = invokeGuardedCallbackDev;
chrome
下Pause on exceptions
功能
操作: f12 -> Source Tab -> 点击 Pause on exceptions 暂停图标 -> 图标变成蓝色,表明启用了在未捕获到的异常出现的时候断点的功能。
勾选Pause On Caught Exceptions
, 能够在捕获到异常的情况下也断点。
try{
throw'a exception';
}catch(e){
console.log(e);
}
上面 try
里面的代码会遇到异常,但是后面的 catch
代码能够捕获该异常。如果是所有异常都中断(勾选了Pause On Caught Exceptions
),那么代码执行到会产生异常的 throw 语句时就会自动中断;而如果是仅遇到未捕获异常才中断,那么这里就不会中断。一般我们会更关心遇到未捕获异常的情况。