浅谈“分治”在React架构设计中的实践

1,185 阅读9分钟

前言

可能有些标题党的意思了,哈哈哈。不过别急着关掉页面,先看看试试吧。

大家好,我是岸汀,是一名前端小学生。前主管离开前告诫我,做架构一定要不断的反思,不断问自己 Why。这篇文章就是记录最近在做项目架构设计时候的一些思考。

本文不涉及React源码分析,只是简单的从 React 架构设计上简单的谈谈个人的一些思考。

分治是什么

分治就是“分而治之”。这是一种设计思想,在算法设计中很常见。具体含义就是将原问题划分成 n 个规模较小而结构与原问题相似的子问题。

我理解的分治

我认为 分治 可以简单理解为拆分,只要做了拆分,就算 分治。本文所讲的东西都是 拆分。可能与各位码农同学日常所接触到的 分治 有点区别吧

分治有什么作用

回答这个问题之前,我先举几个生活中分治的例子,从例子出发,最后再总结答案。

分治在生活中的体现

一、双向车道

如果车道不分正向、反向可以吗?答案肯定是可以的。乡间小道就是这样的,不区分正向反向。那为什么城市里的车道要区分正向反向呢?因为这样方便管理,可以提升交通通过性。

所以分治在这里通过拆分,将交通管理问题简化了,方便管理。

image.png 【图片源于网络】

二、 人口普查

全国人口普查,将问题拆分成省级,市级,区级,乡镇级,街道级,小区级。由各个小区进行实地探访,统计人数。最终逐级上报,将全国所有的数据统计起来就完成了人口普查。

所以分治在这里通过将大问题逐级拆分成一个一个可以相对简单的小问题,最终聚合起来解决了一个大问题。

image.png

【图片源于网络】

分治作用的总结

总结一下以上两个例子,分治都是在将一个复杂难以解决的问题,通过拆分,化解为可以简单解决的小问题,最终聚合起来解决了一个复杂的问题。

所以,分治的作用就是帮我们解决复杂问题。

从React的架构设计来看分治

我讲的都是 React@16.8+。本人对于 React 架构也是一知半解,如果说错了,还请轻喷。

React 架构设计中,到处充斥着分治思想,这里我会从顶层设计到下层架构设计来分析。

从 React 顶层设计来看

架构划分

React 大致可以划分为三个部分:

  • Scheduler(调度器)—— 调度并计算任务的优先级
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到界面上

具体如何渲染到页面上,有很多种实现方式,在 web 端就是 react-dom,在 APP 端是 react-native。其他还有 react-canvas、react-svg 等。

image.png 【图片源于知乎

总结

从顶层设计上来说,React 将一个复杂的工作,拆分成三个部分来实现,实际上也是分治的一种体现。三个部分之间各自负责一块复杂的工作,然后有机的结合起来,完成一个复杂的任务。

从 Fiber Reconciler 架构设计来看

fiber 本身可以有三层含义:

  1. 作为一个静态的JS数据结构,就是一个简单的JS对象实例,上面有许多的方法和属性。
  2. 作为一个动态的工作单元,在 React 实例运行中,节点的增删改查、节点的变更与计算都是通过 fiber 工作单元来实现。
  3. 作为一种架构,在实例运行过程中,通过双缓存树的设计,来完成对变化部分的 diff,再将变化的部分更新到界面上。

Fiber Reconciler 架构是什么

Fiber Reconciler 是 React16 引入的一个新特性架构,用于解决CPU瓶颈问题。具体来说就是将 React 运行中的 组件、DOM节点 等都通过 Fiber节点 来保存,在递归更新过程中,数据都是通过 Fiber 节点来存取。

Fiber Reconciler 的调度方式 image.png 【图片源于网络

Stack Reconciler 的调度方式 image.png 【图片源于网络

为什么要有 Fiber Reconciler 架构

没有 Fiber 架构可以吗?其实是可以的,React15 以及更低的版本用的是 Stack Reconciler 架构,同样能够构建出大型应用。

那为什么要有 Fiber 架构呢?原因就是CPU瓶颈导致了React性能受限,在新架构解决性能问题时,引入了Fiber架构。这里简单解释一下性能受限以及解决方式。

当更新节点过多时,受限于JS的单线程,Stack Reconciler 会长期占用CPU用于计算,一旦计算时间超过浏览器一帧(以chrome为例,按照刷新率为60HZ来算,一帧时间就是 16ms)的时间,页面就会出现卡顿。解决卡顿的方式有两种:

  1. 绕过单线程限制,使用 Worker 来做计算,主线程只用于渲染和处理交互。
  2. 同步变异步,将同步计算变为异步可中断计算,利用浏览器的事件循环机制,自动调度计算和渲染。

而 React 采用了第二种方式,异步可中断的计算。在实现这种方式的时候,就引入了 Fiber 架构。

至于为什么没有采用 Worker 这种方式来做,我猜测是两方面的原因,一方面是 Worker 太受限了,DOM相关的、Cookie相关的、Window相关的几乎都无法访问。另一方面是线程间传递数据也是一个耗时操作,并且线程间无法传递引用类型(Shared Memory另说),导致事件和 fiber 实例无法传递,从而无法正常进行 diff 等一系列工作。

总结

从 Fiber Reconciler 架构上来看,异步可中断更新就是将一个难以解决的问题,通过拆分成异步的计算来解决 CPU 瓶颈问题,这种拆分也可以看做是分治的思想。

从双缓存树设计上来看

双缓存树是什么

React 实例在运行时,会维护两棵 Fiber 树( Current Fiber 树和 WorkInProgress Fiber 树 )。其中 Current Fiber 树对应的是此时此刻界面上展示的 DOM 树,WorkInProgress Fiber 树则是更新时,在内存中用于计算变更的 Fiber 树。这两棵树都是由 Fiber 节点组成,所有都是存在于内存中的数据。

image.png 【图片源于网络

为什么要有双缓存树

可以没有吗?这个问题我没仔细研究过,但是我猜测应该是有办法只使用一棵树就能解决问题的,但是那样复杂度估计会飙升。

所以引入双缓存树设计,其中有一个原因应该就是降低复杂度。这里不得不提到大名鼎鼎的 diff 算法了。这个算法其实就是用来决策到底是复用 Current Fiber 树上的 Fiber 节点,还是更新这个 Fiber 节点。

总结

双缓存树肯定是可以降低复杂度的,通过将状态拆分并保存在两棵树上,然后每次更新通过 diff 出变化的节点,再更新到界面上。这也是分治的思想。

从生命周期设计上来看

React 组件的生命周期设计

React 的生命周期是指组件运行的三个阶段:挂载(Mount)、更新(Update)、卸载(Unmount)。每个阶段又可以细分为多个更小的阶段。

image.png 【图片源于网络

生命周期设计的好处

从架构层面来支持生命周期,提供不同阶段的多个 生命周期钩子函数,能够给与组件更丰富的表现能力与定制化能力。

如果架构上不支持生命周期钩子,我们就不得不自己维护了,组件维护的复杂度可能一下子就上去了。

总结

生命周期设计其实也是将组件的运行状态拆分成多个阶段,用来降低组件设计本身的复杂度。这里其实也包含了分治的思想。

从组件拆分上来看

组件可以不拆分吗

可以。不知道大家有没有见过一种组件,整个复杂页面乃至整个应用只有一个组件,这个组件只有一个自身方法,然后所有代码都写在 render 中,所有逻辑全在 render 中实现。

class XXX extends React.Component {
    render () {
        return (
            // 此处省略 2000 行代码
        )
    }
}

这种代码有问题吗?

可能有问题吧,至少我的认知会判断是有问题的。

也可能没有问题吧,毕竟这个代码能运行。

组件拆分是在做什么

组件拆分是按照不同的功能、不同的逻辑单元、不同的职责等来拆分并封装到单独的模块中。也有可能是因为团队的代码规范,要求不同细粒度的拆分并封装组件。前者可以大幅度的降低调试难度,后者则可能是更美观吧。

image.png

总结

将复杂的页面结构、复杂的逻辑拆分并封装到不同组件中,其实也包含了分治的思想,能够将复杂问题简化。

结语

分治的目的

分治的目的都是为了将复杂问题简单化,将难以解决的问题拆分为可以解决的简单问题。

分治无处不在

在生活中,大量充斥着分治的设计思想。

在工作中,作为新时代的农民工,每天面对的代码中也充斥着分治的思想。例如:代码中的 if else 可能就包含着你在程序设计时的分治思考。

给我的一些启发

做架构是在做什么

我个人狭义的理解:架构的本质是面向需求的解决方案,所以不同的项目会有不同的架构。做架构是在为项目可长期维护来负责。

架构师具体做的是:能够把一个复杂的大系统真正想清楚和透彻,同时还能够把大系统拆散为松耦合的多个模块,最终又在各个模块独立并行完成开发后,能够通过预先定义的接口将这些模块又组装和集成起来。

人人都可以是架构师

架构是面向需求来做的,不了解需求的架构师无法设计出合理且合适的架构。工程师在实现需求时其实也可以是需求的架构师,同样的需求有不同的实现方式,如何优雅的实现一套可维护的代码也能考验一个人的架构能力。

特别感谢

卡颂 - 魔术师卡颂