React的操作系统梦,任重道远

时间:2022-01-01 02:29:06

React的操作系统梦,任重道远

这篇文章包括如下内容:

简要回顾下React从16~21年的迭代历程

React为什么对新特性(Concurrent Mode)有这么大执念

为什么当前社区项目/库要升级到Concurrent Mode比较困难

迭代历程回顾

 

React Core Team从16年开始改造React的核心模块Reconciler(diff算法会在该模块执行)。

经过一年多的改造,将其从流程不可中断的「递归实现」(被称为Stack Reconciler)改为流程可中断的「遍历实现」(被称为Fiber Reconciler)。

在此之后,基于Fiber Reconciler,实现了一套可以区分任务优先级的机制,大体原理如下:

不同交互(用户点击交互/请求数据/用户拖拽...)触发的状态更新(比如调用this.setState)会拥有不同优先级,在源码内对应一个时间戳变量expirationTime。

React会根据expirationTime的大小调度这些更新,最终实现的效果为:「用户交互」触发的更新会拥有更高的优先级,先于「请求数据」触发的更新。

高优先级意味着该更新对DOM产生的影响会更快呈现在用户面前。

在此之后,React Core Team发现基于expirationTime的调度算法虽然能满足fiber树的整体优先级调度,但是不够灵活(比如无法满足局部fiber树的优先级调度(例如Suspense))。

具体原因见这篇文章:启发式更新算法

所以去年React Core Team的Andrew Clark将expirationTime模型重构为以一个32位二进制的位代表优先级的lane模型。

React的操作系统梦,任重道远

PR参见Initial Lanes implementation #18796[1]

如果你是个React重度用户,让你聊聊这些年React的重大变化,可能你会说:

  • Context API重构
  • Hooks

但从我们上面讲到的内容来看,从16年到21年,React底层其实做了大量重构工作。

有人问:做了这么多重构,React开发者居然一点感知都没有?

是的,即使当前稳定版本的React底层已经支持时间切片、支持更智能的更新合并机制(batchedUpdates)。

但是React内部有很多裹脚布一样的代码让新架构的行为表现的与老架构(Stack Reconciler)一致。

React Core Team的执念

 

就像开发业务的开发者需要背负OKR,强如React Core Team成员,也会为OKR苦恼。

20年的React圣诞特辑,React Core team的Rachel Nabors小姐姐就在文章Inside the React Core team[2]中表示:

不能因为你没有产出就代表你没有价值(一把辛酸泪)。

React的操作系统梦,任重道远

作为视图层的库,在不开大脑洞的情况下,React能做的已经趋于极致了。

协程、并发这些操作系统中的概念被搬进React,函数式编程的理念也在React中落地(Hooks)。

React该何去何从?

React的灵魂人物、Hooks的作者、同时也是TC39成员Sebastian Markbåge给出的答案是:

向后、向BFF层发展

React的操作系统梦,任重道远

简单的说:

在SSR领域,当前的实现方案还比较粗犷:

  1. 组件在服务端编译成模版字符串(脱水)
  2. 前端渲染模版字符串
  3. 完成组件的可交互(注水)与余下的渲染

这样的SSR方案粒度不够细,如果Fiber Reconciler能将时间切片的粒度控制在组件级别,SSR的粒度为什么不能控制在组件级别呢?

要达到这个目标,起码需要支持:

  • 一套React组件的流式数据传输协议(区别于字符串模版)
  • 前端能精确控制组件的状态(加载中/加载失败/加载成功),即Suspense特性

而Suspense特性依赖Concurrent Mode的时间切片特性。

没有社区的大量库接入Concurrent Mode,使时间切片成为默认配置,Sebastian Markbåge的远大理想(OKR)无异于空中楼阁。

React的操作系统梦,任重道远

所以,当务之急是让社区尽快跟上React升级的步伐。

升级Concurrent Mode的难点

 

当前社区大量React生态库的逻辑都是基于如下React运行流程:

  1. 状态更新 --> render --> 视图渲染 

如果React的运行流程变为:

  1. 状态更新 --> render(可暂停) --> 视图渲染 
  2.  
  3. 或 
  4.  
  5. 状态更新 --> render(中断)--> 重新状态更新 --> render(可暂停) --> 视图渲染 

会发生什么?

会发生一种被称为tearing的现象,我们来举个例子:

假设我们有一个变量externalSource,初始值为1。

1000ms后externalSource会变为2。

  1. let externalSource = 1; 
  2.  
  3. setTimeout(() => { 
  4.     externalSource = 2; 
  5. }, 1000) 

我们有个组件A,他渲染的DOM依赖于externalSource的值:

  1. function A() { 
  2.   return <p>{externalSource}</p>; 

在当前版本的React中,在我们的应用中组件树的不同地方使用A组件,会出现某些地方的DOM是<p>1</p>,某些地方是<p>2</p>么?

答案是:不会。

因为当前React的如下运行流程是同步的:

  1. 状态更新 --> render --> 视图渲染 

使externalSource变为2的setTimeout会在这个流程对应的task(宏认为)执行完后再执行。

但是当切换到Concurrent Mode:

  1. 状态更新 --> render(可暂停) --> 视图渲染 

当render暂停时,浏览器获得JS线程控制权,就会执行使externalSource变为2的setTimeout。

这样可能不同的A组件渲染出的p标签内的数字不一样。

这种由于React运行流程变化,导致依赖外部资源时,状态与视图不一致的现象,就是tearing。

这里改变externalSource的外力,可能来自于各种task(IO、setTimeout...)

  • 当前有个解决外部资源状态同步的提案useMutableSource[3]
  • 这个库will-this-react-global-state-work-in-concurrent-mode[4]测试了主流状态管理库是否会导致tearing

艰难的小步前进

 

为了让开发者能渐进、少点痛苦的升级到Concurrent Mode,React Core Team一直在努力:

  • 提供StrictMode(严格模式)组件,规范开发者行为
  • 将componentWilXXX标记为unsaft_
  • 提供渐进的升级路线,(从legacy模式到blocking模式到concurrent模式)

显然,React Core Team觉得社区的升级速度还是太慢了。

最近,一个新的PR被合入:Make time-slicing opt-in[5]

这个PR中提到:在下个主版本中,会全量Concurrent Mode,但是这个Concurrent Mode会默认关闭时间切片功能。

就差直接喊话开发者:各位大爷们,求求你们快升级吧,OKR就指着他了

这种悲伤、殷切、又期待的心情直接导致了提交这次PR的Ricky小哥逐渐沙雕(狗头保命):

React的操作系统梦,任重道远

React的操作系统梦,任重而道远啊~~~

参考资料

 

[1]Initial Lanes implementation #18796:

https://github.com/facebook/react/pull/18796

[2]Inside the React Core team:

https://react.christmas/2020/24

[3]useMutableSource:

https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md

[4]will-this-react-global-state-work-in-concurrent-mode:

https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode

[5]Make time-slicing opt-in:

https://github.com/facebook/react/pull/21072

原文地址:https://mp.weixin.qq.com/s/Syyl1T4n4KyuGQH-KZKDQg