# 如何解释 React 的渲染流程

React 的渲染过程大致一致,但协调并不相同,以 React 16 为分界线,分为 Stack Reconciler 和 Fiber Reconciler。这里的协调从狭义上来讲,特指 React 的 diff 算法,广义上来讲,有时候也指 React 的 reconciler 模块,它通常包含了 diff 算法和一些公共逻辑。

回到 Stack Reconciler 中,Stack Reconciler 的核心调度方式是递归。调度的基本处理单位是事务,它的事务基类是 Transaction,这里的事务是 React 团队从后端开发中加入的概念。在 React 16 以前,挂载主要通过 ReactMount 模块完成,更新通过 ReactUpdate 模块完成,模块之间相互分离,落脚执行点也是事务。

在 React 16 及以后,协调改为了 Fiber Reconciler。它的调度方式主要有两个特点,第一个是协作式多任务模式,在这个模式下,线程会定时放弃自己的运行权利,交还给主线程,通过requestIdleCallback 实现。第二个特点是策略优先级,调度任务通过标记 tag 的方式分优先级执行,比如动画,或者标记为 high 的任务可以优先执行。Fiber Reconciler的基本单位是 Fiber,Fiber 基于过去的 React Element 提供了二次封装,提供了指向父、子、兄弟节点的引用,为 diff 工作的双链表实现提供了基础。

在新的架构下,整个生命周期被划分为 Render 和 Commit 两个阶段。Render 阶段的执行特点是可中断、可停止、无副作用,主要是通过构造 workInProgress 树计算出 diff。以 current 树为基础,将每个 Fiber 作为一个基本单位,自下而上逐个节点检查并构造 workInProgress 树。这个过程不再是递归,而是基于循环来完成。

在执行上通过 requestIdleCallback 来调度执行每组任务,每组中的每个计算任务被称为 work,每个 work 完成后确认是否有优先级更高的 work 需要插入,如果有就让位,没有就继续。优先级通常是标记为动画或者 high 的会先处理。每完成一组后,将调度权交回主线程,直到下一次 requestIdleCallback 调用,再继续构建 workInProgress 树。

在 commit 阶段需要处理 effect 列表,这里的 effect 列表包含了根据 diff 更新 DOM 树、回调生命周期、响应 ref 等。

但一定要注意,这个阶段是同步执行的,不可中断暂停,所以不要在 componentDidMount、componentDidUpdate、componentWiilUnmount 中去执行重度消耗算力的任务。

如果只是一般的应用场景,比如管理后台、H5 展示页等,两者性能差距并不大,但在动画、画布及手势等场景下,Stack Reconciler 的设计会占用占主线程,造成卡顿,而 fiber reconciler 的设计则能带来高性能的表现。

# React 的渲染异常会造成什么后果?

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

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

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

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

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