• 前端面试宝典React篇12 React 的渲染异常会造成什么后果?


    面试中,在问到 React 的渲染流程后,通常还会问到“React 的渲染异常会造成什么后果”,大部分应聘的同学,可能会说“页面会崩掉”,但崩掉后是什么样子,又因为什么而崩掉就说不清楚了。这一讲我们就来讲解这个问题应该如何回答。

    破题

    在第 03 讲“如何避免生命周期中的坑”中,有讲到“错误边界”的相关内容:如果渲染异常,在没有任何降级保护措施的情况下,页面会直接显示白屏。但问题到这里并没有结束,在类似“会产生什么影响”或者“会造成什么后果”的问题后,紧接着会询问你“怎么做”,这就需要你给出一个方案来解决问题。所以这类问题的中心会发生偏移,从“是什么”到“怎么做”,即从“解释原理”到“提供方案”。

    那如何评判方案的优劣呢?通常有这样三种层次。

    解决问题

    在表达方式上,倾向于展示自己有解决该问题的能力,如“我在项目 x 中遇到了一个 y 问题,通过排查 a、b、c 三个点找到了问题所在,最后改动了项目 x 中的 d、e、f 点完成了上线修复”这样的回答。

    预防问题

    在表达方式上倾向于暗示自己是一个老手,在问题处理中以预防为主。通常会讲自己在初始化项目后,会做这样几条措施以预防同类的低级错误。

    工程化方案

    上面两种表达还停留在个人层次的基本面,缺乏全盘考虑。你要知道,我们做前端实际上也是在做工程,那我们就需要从工程化的角度去思考方案该怎么做。那什么是工程化呢?工程化这个概念并没有统一的标准说法,但我们仍然可以从项目经验中进行总结提炼。

    工程化通常以标准化为抓手完成降本增效,它涉及的方向有:

    • 模块标准化

    • 流程标准化

    • 代码风格标准化

    • ……

    通过标准化的方案解决同类型的问题。即在解决的过程中需要考虑减少人为因素,引入自动化过程,比如在统一团队内部代码风格时,我们并不会用人人互相 Review 代码的方式去检查大家的符号与缩进,而是使用 ESLint 自动化处理。所以在解决同类型问题时,沉淀的标准化方案,才能用于更多的业务方向,在团队内部才更具有通用价值。

    只解决自己所遇到的问题,贡献度是很低的,如果能够产出通用的方案供团队使用,就非常有价值了。同时是否具备团队视野会影响面试官对个人能力的评定。所以在面试中,不少面试官更偏好这类问题。他们希望从这类问题中了解应聘者在原团队所处的位置,以及所做的贡献是否能匹配更高的职级。这就又涉及了另外一个问题,即如何量化成果?项目价值也好,业绩贡献也好,最后都需要数据支撑,如果只是空口一句修复了 Bug,而没有提供任何参考性的指标,那就没有任何意义。工程是一门重视过程与结果的科学,不仅需要注重实施方案,也需要注重结果的量化。

    审题

    从上面的分析中可以整理出两个答题方向:

    • “是什么”,即阐述现象与原理,在描述原理时,也要注意提供相符的案例;

    • “怎么解决”,即从工程化的角度展开,拿出通用化方案,并能量化结果,给出数据与指标。

    Drawing 2.png

    入手

    是什么

    首先我们需要了解渲染异常的现象是怎样的。下面的案例就能够很好地说明:

    import React from "react";
    export default class App extends React.Component  {
      state = {
        fruits: ["apple", "banana", "pear", "orange"]
      };
      handleClick = () => {
        this.setState({
          fruits: undefined
        });
      };
      render() {
        return (
          <div className="App">
            <ul>
              {this.state.fruits.map((fruit) => (
                <li>{fruit}li>
              ))}
            ul>
            <button onClick={this.handleClick}>Make a chaosbutton>
          div>
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这段代码包含了两个部分:

    • 渲染 this.state.fruits 的常规操作;

    • 按钮点击事件,通过点击按钮将 fruits 重置为 undefined 。

    整段代码运行之后,展示的内容如下图所示。

    Drawing 3.png

    这时如果点击按钮,就会看到熟悉的 React 报错。这是因为在开发模式下 react-error-overlay,所以在代码抛出异常后,能够在页面上直接看见。

    Drawing 4.png

    如果是生产模式呢?即直接把代码打包上线,运行在线上会怎么样呢?就像下面动图所展示的一样,你能看到整个界面会直接消失。

    react面试12.gif

    那为什么会产生这样的现象呢?React 官方团队是这样回答的。

    组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时产生可能无法追踪的 错误。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。
    
    • 1

    简而言之,如果有 JavaScript 的错误出现在 React 内部渲染层,就会导致整个应用的崩溃。从现象上来看,就是整个界面从页面中被移除掉,也就展现出了所谓的白屏。

    怎么做

    通用方案

    那该怎么办呢?第 03 讲“如何避免生命周期中的坑”中有提到错误边界的概念,其中应用到了 getDerivedStateFromError 与 componentDidCatch 两个函数。但这并不是完整的方案,正如前面所说,需要考虑整体性。而完整方案通常是从预防与兜底两个角度下手解决问题。

    预防

    预防就需要先知道病灶在哪里。既然是渲染异常,就需要分析异常会出现在哪里。我们可以知道,在渲染层,也就是 render 中 return 后的 JSX,都是在进行数据的拼装转换

    • 如果在拼装的过程中出现错误,那直接会导致编译的失败;

    • 但如果在转换的过程中出现错误,就很不容易被发现。

    比如上面案例中的 this.state.fruits 为 undefine 就很难被发现,尤其是前端的渲染数据基本上都是通过后端业务接口获取,那么数据是否可靠就成了一个至关重要的问题。

    这个问题被称为 null-safety,也就是空安全。它并没有最优解,不仅困扰着我们,也困扰着像 Facebook 这样的大公司,目前他们对于这个问题的解决方案是使用 idx。

    idx 在使用时需要配置 Babel 插件,再引入 idx 库,然后通过 idx 函数包裹需要使用的 object,再在回调函数中取需要的值。

    import idx from 'idx';
    // 使用时
    function getFriends() {
      return idx(props, _ => _.user.friends[0].friends)
    };
    // 转换后
    function getFriends() {
      return props.user == null ? props.user :
      props.user.friends == null ? props.user.friends :
      props.user.friends[0] == null ? props.user.friends[0] :
      props.user.friends[0].friends
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    idx 的代码既不优雅,也不简洁,还需要引入 Babel 插件,所以在社区中使用者寥寥无几,还不如使用 Lodash 的 get 函数。那有没有优雅的解决方案呢?有,就是 ES2020 中的 Optional chaining,中文叫作可选链操作符。如果用可选链操作符重新 render 函数的话,可以写成这样:

      render() {
        return (
          
      {this.state?.fruits.map((fruit) => (
    • {fruit}
    • ))}
    );
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    并不是所有浏览器都支持该特性,但可以通过引入 Babel 插件保障浏览器兼容性。

    {
      "plugins": ["@babel/plugin-proposal-optional-chaining"]
    }
    
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    可选链操作符是一个遵循 ES 标准、侵入性比较低的方案。那有没有无须配置的方案呢?也是有的,如果是使用 TypeScript 的话,在 3.7 版本中可以直接使用该特性。

    兜底

    虽然空安全的预防方案都足够完善,但我们不可能在每行代码中都使用,而且旧项目的老代码,逐行修改,也费时费力。所以以防万一,肯定是需要兜底方案的。

    兜底应该限制崩溃的层级。错误边界加到哪里,崩溃就止步于哪里,其他组件还可以正常使用,所以只需要给关键的 UI 组件添加错误边界,那就可以应用到我们在第 05 讲“如何设计 React 组件?”中提到的高阶组件:

    export const errorBoundary = (WrappedComponent) => {
        return class Wrap extends Component {
          state = {
    	        hasError: false,
    	  };
          static getDerivedStateFromError(err) {
    	        return {
    	            hasError: true,
    	            err
    	        };
    	    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
        componentDidCatch(err: Error, info: React.ErrorInfo) {
            console.log(err, info);
        }
    
      render() {
        return (
          this.state.hasError ? <ErrorDefaultUI error={this.state.error} /> : <WrappedComponent {...this.props} />
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    }

    用这个高阶组件拦截报错信息,展示统一的错误页面,也就是 ErrorDefaultUI。使用起来也很简单,直接在原函数上挂上注解就可以了。

    @errorBoundary
    export default class UserProfile {
    }
    
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    这个方案可以在团队内部抽取为一个 npm 包,提供能力复用。

    量化结果

    我们做了这么多事情,那如何评估做得好不好呢?就需要用数据说话,来评估做的事到底有没有价值。

    • 在预防层面,需要看空安全方案在项目中的覆盖量,从而保证团队内项目都将空安全用了起来。

    • 在兜底层面,同样需要保障方案在项目中的覆盖量,其次需要统计兜底页面成功兜底的次数,最后兜底页面展示时,能够及时完成线上报警。

    所以量化结果的工作主要是从覆盖统计两个方向展开。

    覆盖量怎么计算呢?最简单的方法就是直接查看项目的 package.json 文件是否引入相关的库。排查的方案因不同的公司而异。比如有的公司代码检测使用统一的工具,比如 sonar,那你就只需要去写配置文件就可以了。那如果公司什么都没接入呢?就需要结合公司的实际情况了,你甚至可以用一个最原始简单的方案去实现,比如写一个 node 脚本,去拉取相关仓库的代码自行分析,然后每周产出一个 dashboard 查看使用情况。

    统计相对容易一些,因为每个公司至少会接入一种统计工具,比如用百度统计、Google 统计或者 mixpanel 来完成业务分析。所以只需要在代码中,添加一行统计代码,就像下面这段代码一样直接上报相关信息就可以。如果有条件的话,最好也接入一下报警系统,这样能够及时发现问题并上报。

    componentDidCatch(err: Error, info: React.ErrorInfo) {
        // 业务统计
        trackEvent('error boundary', { err, info });
        // 业务报警
        reportError(error, info)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    到这里数据就有了,就可以支撑你的汇报工作了。

    答题

    React 渲染异常的时候,在没有做任何拦截的情况下,会出现整个页面白屏的现象。它的成型原因是在渲染层出现了 JavaScript 的错误,导致整个应用崩溃。这种错误通常是在 render 中没有控制好空安全,使值取到了空值。

    所以在治理上,我的方案是这样的,从预防与兜底两个角度去处理。

    在预防策略上,引入空安全相关的方案,在做技术选型时,我主要考虑了三个方案:第一个是引入外部函数,比如 Facebook 的 idx 或者 Lodash.get;第二个是引入 Babel 插件,使用 ES 2020 的标准——可选链操作符;第三个是 TypeScript,它在 3.7 版本以后可以直接使用可选链操作符。最后我选择了引入 Babel 插件的方案,因为这个方案外部依赖少,侵入性小,而且团队内没有 TS 的项目。

    在兜底策略上,因为考虑到团队内部和我存在一样的问题,就抽取了兜底的公共高阶组件,封装成了 NPM 包供团队内部使用。

    从最终的数据来看,预防与治理方案覆盖了团队内 100% 的 React 项目,头三个月兜底组件统计到了日均 10 次的报警信息,其中有 10% 是公司关键业务。那么经过分析与统计,首先是为关键的 UI 组件添加兜底组件进行拦截,然后就是做内部培训,对易错点的代码进行指导,加强 Code Review。后续到现在,线上只收到过 1 次报警。

    Drawing 7.png

    总结

    本讲中的问题与方案都不复杂,但着重强调了工程思维。你会发现在有基本数据支撑下,去描述一件事,会更立体,让人更容易理解这件事的价值,也更加让人信服。

    当然工程思维不是只能回答该讲的问题,它还可以解决很多其他问题,所以在日常工作和生活中,你可以思考下哪些问题是可以用这种思维来回答的,欢迎在留言区留下你的答案。

    在下一讲中,我将会介绍如何通过描绘基线来完成性能优化。


    《大前端高薪训练营》


    精选评论

    **婷:

    npm包里不用装react的相关依赖吗

        讲师回复:

        建议在 package.json 中的 peerdependencies 字段里声明 react 的最低使用版本即可。

    **杰:

    封装成npm,好奇难道包里面就上面所写的一个高阶函数吗

        讲师回复:

        对,这是非常通用的代码共享方式,很多 npm 包内部都只有一个函数,比如 https://github.com/component/debounce 里面也就一个 debounce 函数。
    封装给公司内部使用的库,不仅要考虑功能,还要考虑数据采集,比如埋点啊,业务统计等,都是需要做的。这样更好向上汇报。

    **举:

    很棒,解决了实际问题,通过埋点得出的统计数据容易让结果得到量化,量化的报告结果容易让领导满意。

        编辑回复:

        get到了精髓,真棒

    **林:

    通俗易懂,高屋建瓴

        编辑回复:

        哇奥,评论言简意赅,但是一针见血,棒

  • 相关阅读:
    Linux安装及使用
    软件考试学习笔记(希赛)
    【Linux学习笔记1】-【网络连接的三种模式:桥接模式、NAT模式、主机模式,克隆虚拟机,虚拟机快照,安装vmtools,设置共享文件夹】
    二层交换机和三层交换机
    浅议飞机状态监控
    105. 从前序与中序遍历序列构造二叉树
    主流开发语言和开发环境介绍
    在C#中,如何以编程的方式设置 Excel 单元格样式
    MySQL锁
    蓝桥杯刷题(三)
  • 原文地址:https://blog.csdn.net/fegus/article/details/126801925