这篇文章包括如下内容:
-
简要回顾下
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
重度用户,让你聊聊这些年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中表示:
不能因为你没有产出就代表你没有价值(一把辛酸泪)。
作为视图层的库,在不开大脑洞的情况下,React
能做的已经趋于极致了。
协程
、并发
这些操作系统中的概念被搬进React
,函数式编程的理念也在React
中落地(Hooks
)。
React
该何去何从?
React
的灵魂人物、Hooks
的作者、同时也是TC39
成员Sebastian Markbåge
给出的答案是:
向后、向
BFF
层发展
简单的说:
在SSR
领域,当前的实现方案还比较粗犷:
-
组件在服务端编译成模版字符串(脱水)
-
前端渲染模版字符串
-
完成组件的可交互(注水)与余下的渲染
这样的SSR
方案粒度不够细,如果Fiber Reconciler
能将时间切片
的粒度控制在组件级别,SSR
的粒度为什么不能控制在组件级别呢?
要达到这个目标,起码需要支持:
-
一套
React
组件的流式数据传输协议(区别于字符串模版) -
前端能精确控制组件的状态(加载中/加载失败/加载成功),即
Suspense
特性
而Suspense
特性依赖Concurrent Mode
的时间切片
特性。
没有社区的大量库接入Concurrent Mode
,使时间切片
成为默认配置,Sebastian Markbåge
的远大理想(OKR
)无异于空中楼阁。
所以,当务之急是让社区尽快跟上React
升级的步伐。
升级Concurrent Mode
的难点
当前社区大量React
生态库的逻辑都是基于如下React
运行流程:
状态更新 --> render --> 视图渲染
如果React
的运行流程变为:
状态更新 --> render(可暂停) --> 视图渲染
或
状态更新 --> render(中断)--> 重新状态更新 --> render(可暂停) --> 视图渲染
会发生什么?
会发生一种被称为tearing
的现象,我们来举个例子:
假设我们有一个变量externalSource
,初始值为1。
1000ms后externalSource
会变为2。
let externalSource = 1;
setTimeout(() => {
externalSource = 2;
}, 1000)
我们有个组件A
,他渲染的DOM
依赖于externalSource
的值:
function A() {
return <p>{externalSource}</p>;
}
在当前版本的React
中,在我们的应用中组件树的不同地方使用A
组件,会出现某些地方的DOM
是<p>1</p>
,某些地方是<p>2</p>
么?
答案是:不会。
因为当前React
的如下运行流程是同步的:
状态更新 --> render --> 视图渲染
使externalSource
变为2的setTimeout
会在这个流程对应的task
(宏任务)执行完后再执行。
但是当切换到Concurrent Mode
:
状态更新 --> render(可暂停) --> 视图渲染
当render
暂停时,浏览器获得JS
线程控制权,就会执行使externalSource
变为2的setTimeout
。
这样可能不同的A
组件渲染出的p
标签内的数字不一样。
这种由于React
运行流程变化,导致依赖外部资源时,状态与视图不一致的现象,就是tearing
。
这里改变externalSource
的外力,可能来自于各种task
(IO
、setTimeout
...)
当前有个解决外部资源状态同步的提案useMutableSource
这个库will-this-react-global-state-work-in-concurrent-mode测试了主流状态管理库是否会导致
tearing
艰难的小步前进
为了让开发者能渐进、少点痛苦的升级到Concurrent Mode
,React Core Team
一直在努力:
-
提供
StrictMode
(严格模式)组件,规范开发者行为 -
将
componentWilXXX
标记为unsaft_
-
提供渐进的升级路线,(从
legacy
模式到blocking
模式到concurrent
模式)
显然,React Core Team
觉得社区的升级速度还是太慢了。
最近,一个新的PR
被合入:Make time-slicing opt-in
这个PR
中提到:在下个主版本中,会全量Concurrent Mode
,但是这个Concurrent Mode
会默认关闭时间切片
功能。
就差直接喊话开发者:各位大爷们,求求你们快升级吧,OKR
就指着他了😭
这种悲伤、殷切、又期待的心情直接导致了提交这次PR
的Ricky
小哥逐渐沙雕(狗头保命):
React
的操作系统梦,任重而道远啊~~~