第十六章 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项目,我们将会无法捕获大部分的报错,或者把每一个组件写的过于复杂难懂,甚至因此带来其他报错...

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

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu8 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss