第十六章 React中常用的的错误处理方法 【上】

身为开发者,我们都希望应用稳定运行,没有任何bug。但这是不可能的,我们是人类,都会犯错。无论我们多么的仔细,写了多少自动化测试,总是会写出一些bug。最重要的是,当事故发生时,能够快速定位这个bug,并优雅地修复这个bug。

在最后一章,我们会看看React是如何进行错误处理的:

  • 错误发生时,我们可以做什么?
  • 在诸多错误处理方法中,有什么要注意的吗?
  • 如何降低这些错误带来的风险?

为什么要在React中捕获错误

首先要搞明白的是,为什么需要在React中设置捕获错误的API?

原因很简单:自React 16以来,任何一个React 生命周期中的错误,都有可能导致整个应用被卸载。在这个版本之前,即使报错发生了,相关组件还是会留在页面,虽然有些功能可能已经不可用了。但是自React 16之后,哪怕是一个不起眼的UI的报错,或者一些外部库的不可控报错,都有可能破坏整个应用,给用户留下一个白屏。

JavaScript是如何捕获错误的?

我们可以使用try/catch语法。try/catch本身就说明其逻辑:在try的块中执行一些代码,如果在此期间发生错误了,就在在catch的块中处理这些错误:

js 复制代码
try {
    // if we're doing something wrong, this might throw an error
        doSomething();
    } catch (e) {
    // if error happened, catch it and do something with it without topping the app
      // like sending this error to some logging service
    }

在处理异步操作时,也可以使用类似的语法:

js 复制代码
try {
    await fetch('/bla-bla');
} catch (e) {
    // oh no, the fetch failed! We should do something about it!
}

或者,我们可以使用老学究风格的promise语法,在catch中处理相关错误:

js 复制代码
fetch('/bla-bla')
.then((result) => {
    // if a promise is successful, the result will be here
    // we can do something useful with it
})
.catch((e) => {
    // oh no, the fetch failed! We should do something about it!
});

这两个方式的概念是一样的,就是在具体实现上有细微不同。所以在这一章剩下的部分,我主要使用try/catch语法。

在React中使用try/catch,以及注意事项

如果一个错误被捕获了,我们应该做点什么,而不仅仅留一个白屏幕给用户:

js 复制代码
const SomeComponent = () => {
    const [hasError, setHasError] = useState(false);
    useEffect(() => {
        try {
          // do something like fetching some data
        } catch (e) {
        // oh no! the fetch failed, we have no data to render! 
        setHasError(true);
    }
});

// something happened during fetch, lets render some nice errorscreen
if (hasError) return <SomeErrorScreen />;
// all's good, data is here, let's render it
return <SomeComponentContent {...datasomething} />;
};

这段代码很简单,在useEffect中调用一个接口。如果在调用过程中发生了错误,设置hasError状态为true,当hasError为true,就会渲染<SomeErrorScreen />组件,为用户提供运维人员的电话号码等。

这段代码是非常直观的,对于简单的fetch来说,它是非常有效的。

但是如果你想捕获这个组件内所有的错误,那你就会发现这段代码的局限性了。

局限性1:在使用useEffect时,会遇到一些麻烦

如果我们用try/catch包裹住useEffect,那么useEffect将不再运作:

js 复制代码
try {
  useEffect(() => {
      throw new Error('Hulk smash');
  }, []);
} catch (e) {
  // useEffect throws, but this will never be called
}

代码会这样运作的原因是,在React中,useEffect是在完成页面渲染后,异步 地调用的,所以在try/catch看来,useEffect这段代码没有任何异常。同样的事情也会发生在Promise:如果我们不等待结果,那么 JavaScript 将会继续执行其后续操作,当 Promise 完成时再返回来处理,并且只会执行 useEffect 内部的代码(或者 Promise 的 then 回调中的代码)。而到那个时候,try/catch 块早就已经执行完毕并结束了。

为了能够捕获到useEffect里发生的错误,try/catch应该被放在useEffect里:

js 复制代码
useEffect(() => {
  try {
    throw new Error('Hulk smash');
  } catch(e) {
    // this one will be caught
  }
}, []);

代码示例: advanced-react.com/examples/16...

这种写法适用于任何使用了useEffect的钩子,或者其他异步允许的代码。同理,为了更好的捕获每一个异常,我们不应该使用单一try/catch来包裹所有,而是针对每一个钩子拆分出try/catch块来捕获异常。

局限性2: 子组件

try/catch无法捕获子组件里发生的任何错误。你不能这样写:

js 复制代码
const Component = () => {
    let child;
    
    try {
        child = <Child />
    } catch (e) {
        // useless for catching errors inside Child component, won't be triggered
    }
    
    return child;
}

这样也不行:

js 复制代码
const Component = () => {
    let child;
    
    try {
        return <Child />
    } catch (e) {
        // still useless for catching errors inside Child component, won't be triggered
    }
}

代码示例: advanced-react.com/examples/16...

这个try/catch发生在我们写<Child />时,此时我们并没有渲染这个组件。我们不过是生成了一个描述对象罢了。描述对象的本质,不过是描述了这个组件的类型、key还有必要属性的对象罢了。

而针对这个属性的渲染,是在try/catch成功执行后发生的,就像刚刚的把useEffect放在try/catch块里面一样。

局限性3: 在没有渲染内容时设置状态

如果你试图在 useEffect 以及各种回调函数之外(也就是在组件的渲染过程中)捕获错误,那么要妥善处理这些错误就不再那么简单了:在渲染过程中是不允许进行状态更新的。

像下面的代码,将会在错误发生时,导致无限重新渲染:

js 复制代码
const Component = () => {
    const [hasError, setHasError] = useState(false);
    
    try {
        doSomethingComplicated();
    } catch (e) {
        // don't do that! will cause infinite loop in case of an error
        // see codesandbox below with live example
        setHasError(true);
    }
}

代码示例: advanced-react.com/examples/16...

我们可以这样,在函数全局内捕获到错误后,返回一个<SomeErrorScreen />组件,而非调用setState

js 复制代码
const Component = () => {
    try {
        doSomethingComplicated();
    } catch (e) {
        // this allowed
        return <SomeErrorScreen />;
    }
}

但正如你可以想象的那样,这有点麻烦,并且迫使我们在同一个组件中以不同的方式处理错误:对于 useEffect 和回调函数使用状态来处理错误,而对于其他所有情况则直接返回。

js 复制代码
// while it will work, it's super cumbersome and hard to maitain, don't do that
const SomeComponent = () => {
    const [hasError, setHasError] = useState(false);
    useEffect(() => {
        try {
            // do something like fetching some data
        } catch (e) {
            // can't just return in case of errors in useEffect or callbacks
            // so have to use state
            setHasError(true);
        }
    });
    
    try {
        // do something during render
    } catch (e) {
        // but here we can't use state, so have to return directly in case of an error
        return <SomeErrorScreen />;
    }
    
    // and still have to return in case of error state here
    if (hasError) return <SomeErrorScreen />;
    
    return <SomeComponentContent {...datasomething} />;
};

概括这个章节:如果我们仅仅是依赖try/catch来开发React项目,我们将会无法捕获大部分的报错,或者把每一个组件写的过于复杂难懂,甚至因此带来其他报错...

好在,还有另一方法!!!

相关推荐
食指Shaye1 分钟前
Chrome 中清理缓存的方法
前端·chrome·缓存
JobsandCzj3 分钟前
PDF 分割工具
javascript·小程序·pdf
午后书香12 分钟前
一天三场面试,口干舌燥要晕倒(二)
前端·javascript·面试
Book_熬夜!28 分钟前
CSS—补充:CSS计数器、单位、@media媒体查询
前端·css·html·媒体
程序员大澈28 分钟前
1个基于 Three.js 的 Vue3 组件库
javascript·vue.js
程序员大澈34 分钟前
3个 Vue Scoped 的核心原理
javascript·vue.js
hyyyyy!38 分钟前
《原型链的故事:JavaScript 对象模型的秘密》
javascript·原型模式
程序员大澈1 小时前
3个好玩且免费的api接口
javascript·vue.js
程序员大澈1 小时前
4个 Vue 路由实现的过程
javascript·vue.js·uni-app
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript