在最近的ReactPR#21488中,核心成员BrianVaughn对React中的一些API和内部标志进行了调整。最显着的变化是:React条目添加了createRootAPI。业内将这一变化解读为:ConcurrentMode(以下简称CM)将在近期稳定下来,并出现在正式版中。React17是稳定CM的过渡版本。一旦CM稳定下来,v18的进度就会大大加快。可以说从2018年到21年,React团队的主要工作都是围绕CM展开的,那么:什么是CM?CM能为React解决什么问题?为什么CM快4年了还不稳定,跨越16和17两个版本?本文将一一解答。什么是CM要了解什么是CM(ConcurrentMode),首先需要知道React源码的运行过程。React大致可以分为两个工作阶段:renderphase在renderphase中计算一个update(通过diff算法)发生变化的部分,因该阶段调用了组件的render函数而得名。渲染阶段可能是异步的(取决于触发更新的场景)。在commit阶段,render阶段计算出来的需要改变的部分,会在commit阶段渲染到view中。对应ReactDOM,appendChild、removeChild等都会被执行。提交阶段必须同步调用(这样用户就不会看到未完全渲染的UI)。我们通过ReactDOM.render创建的应用属于遗留模式。在这种模式下,渲染阶段对应于提交阶段。如果我们通过ReactDOM.createRoot创建的应用(目前稳定版没有这个API),就属于开头提到的CM(并发模式)。CM下,update有优先级的概念,render阶段可能会用高优先级的中断进行update。所以渲染阶段可能会重复多次(被中断后重新启动)。可能有多个渲染阶段对应一个提交阶段。此外,还有一种阻塞模式供开发者逐步从遗留模式过渡到CM。从特性对比可以看出不同模式支持的特性:为什么需要CM?知道什么是CM,那么他有什么用呢?为什么React核心团队花了3年多的时间(从2018年开始)来实现它?这还得从React的设计理念说起。从官网的React哲学我们可以看出React的设计哲学:我们认为React是用JavaScript构建快速响应式大型Web应用的首选方式。其中,快速反应是重点。那么是什么影响了快速反应呢?React团队给出的答案:CPU瓶颈和IO瓶颈CPU瓶颈考虑下面的demo,我们渲染一个3000的列表项:functionApp(){constlen=3000;返回(
{Array(len).fill(0).map((_,i)=>- {i}
)}
);}constrootEl=document.querySelector("#root");ReactDOM.render(
,rootEl);刚才说了,在legacy模式下渲染阶段是不会被打断的,所以这3000个lis的渲染必须在同一个浏览器宏任务中完成。长时间的计算会阻塞线程,导致页面掉帧,这是CPU的瓶颈。解决方案是:启用CM,使渲染阶段可中断,当浏览器一帧的时间所剩无几时,将控制权交给浏览器。等待下一帧的空闲时间再继续渲染组件。IO的瓶颈不仅是长时间计算造成的滞后,网络请求的加载状态也会让页面无法交互,这就是IO的瓶颈。IO瓶颈是客观存在的。作为前端,你能做的就是尽快请求到需要的数据。然而,总的来说:代码可维护性与请求效率是不相容的。什么意思,比如:假设我们封装了请求数据的方法useFetch,通过返回值是否存在来区分是否请求了数据。函数App(){常量数据=useFetch();返回{数据?
:null};}为了提高代码可维护性,useFetch和要渲染的组件User存在于同一个组件App中。但是,如果在User组件中需要更多数据(配置文件数据如下)怎么办?functionUser({data}){const{id,name}=data?.id||{};constprofile=useFetch(id);return(
)}基于代码可维护性原则,使用Fetch和组件Profile来被渲染存在于同一个组件User中。但是,如果这样组织代码,Profile组件只能在Userrender之后进行渲染。数据只能像瀑布中的水一样一层一层往下流。这种请求数据的低效方式称为瀑布。为了提高请求的效率,我们可以将“请求Profile组件需要的数据的操作”提到App组件中,合并到useFetch中:functionApp(){constdata=useFetch();返回{数据?
:null};}但这降低了代码的可维护性(配置文件组件与配置文件数据距离太远)。React团队借鉴了Relay团队的经验,借助Suspense特性提出了ServerComponents。就是在处理IO瓶颈的时候要兼顾代码的可维护性和请求效率。此功能的实现需要不同的CM更新优先级。为什么CM需要这么长时间?下面我们就从源码、特性、生态三个方面来看自下而上的普及CM到底有多难。源码层面的优先级算法改造在v16.13之前,React已经实现了基本的CM功能。正如我们之前所说,CM有一个更新优先级的概念。以前,更新的过期时间是用毫秒数expirationTime标记的。通过比较不同更新的过期时间来确定优先级。通过比较更新的expirationTime和当前时间来判断更新是否过期(过期需要同步执行)。但是expirationTime作为一个与时间相关的浮点数,并不能代表一批优先级的概念。为了实现更高级别的服务器组件功能,需要一批优先级的概念。于是,核心成员AndrewClark开始了对优先级算法旷日持久的改造,参见:PRlanesOffscreensupport同时,另一位成员LunaRuan正在开发一个新的API——Offscreen。可以理解为这是React版本的Keep-Alive特性。在订阅外源和未启用CM之前,以下三个生命周期在一次更新中只会被调用一次:订阅外部源(例如注册事件回调)时,更新可能不及时或内存泄漏。例如:bindEvent是一个基于发布和订阅的外部依赖(比如一个原生的DOM事件):;}componentWillUnmount(){bindEvent('eventA');}render(){return
;}}在componentWillMount中绑定,在componentWillUnmount中解除绑定。当接收到事件时,更新数据。当render阶段反复中断和暂停时,可能会出现:在最终绑定事件之前(bindEvent执行之前),事件源触发了事件,App组件还没有注册事件(bindEvent还没有执行)),那么App获取到的数据就是old。为了解决这个潜在的问题,核心成员BrianVaughn开发了特性:create-subscription来规范React中对外部源的订阅和更新。简单的说就是将外部源的注册和更新与组件在commit阶段的状态更新机制进行绑定。特性级当源代码级支持完成后,基于CM的新特性的开发就会提上日程。这是悬念。[[Umbrella]ReleasingSuspense#13206](https://github.com/facebook/r...PR负责记录Suspense特性的进展。Umbrella标志意味着这个PR将影响许多库、组件和tools可以看到,时间线很长是从18年到最近几天,最开始Suspense只是一个前端特性,那时候ReactSSR只能给前端传递字符串数据(俗称脱水).后来React在SSR传输协议的时候实现了一套组件流,可以流组件,而不仅仅是HTML字符串,这时候Suspense被赋予了更多的职责,它也有更复杂的优先级,这也是其中一个原因刚才说的优先级算法的改造,最后的结果就是今年早些时候推出的ServerComponents的概念,在生态层面,等到源代码支持,特性开发出来,能不能无缝对接,现在还早。已经开了8年了,每次React升级到最后的社区普及,中间都有海量的工作要做。为了帮助社区逐步过渡到CM,React做了以下工作:开发了ScrictMode特性,默认开启,规范开发者的写法,将componentWillXXX标记为不安全,提醒用户不要使用,它将将来被丢弃。提出了一个新的生命周期(getDerivedStateFromProps,getSnapshotBeforeUpdate)来代替上面将被丢弃的生命周期。开发了一种介于传统模式和CM转换之间的中间模式——阻塞模式。这只是过渡过程中最简单的部分。难点在于:如何迁移社区积累的大量基于遗留模式的库?很多动画库和状态管理库(比如mobX)的迁移并不简单。综上,我们介绍了CM的来龙去脉和他迁移的难点。通过这篇文章,你肯定也知道一开始给React添加createRoot(打开CM的方法)是多么的困难。幸运的是,一切都是值得的。如果说React之前的壁垒是:开源时间早,社区大。那么从CM开始,React可能是前端领域最复杂的视图框架了。到那时,不会有任何类似React的框架可以实现与React相同的功能。但是有人说CM这些功能鸡肋,我根本不需要。你觉得CM怎么样?欢迎留下您的讨论。