前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React18,不远啦?

React18,不远啦?

作者头像
公众号@魔术师卡颂
发布2021-07-07 10:17:59
6270
发布2021-07-07 10:17:59
举报
文章被收录于专栏:魔术师卡颂

一系列React源码级视频、文章

React前不久的一次PR #21488中,核心成员「Brian Vaughn」React内一些API、以及内部flag作出调整。

其中最引人注目的改动是:React入口增加createRoot API

业界将这一变化解读为:Concurrent Mode(后文简称为CM)将在不久后稳定,并出现在正式版中。

React17是一个过渡版本,用以稳定CM。一旦CM稳定,那v18的进度会大大加快。

可以说从18年到21年,React团队的主要工作就是围绕CM展开的,那么:

  • CM是什么?
  • CM能解决React什么问题?
  • 为什么经历快4年,跨越16、17两个版本,CM还不稳定?

本文将作出解答。

CM是什么

要了解CM(并发模式)是什么,首先需要知道React源码的运行流程。

React大体可以分为两个工作阶段:

  • render阶段

render阶段会计算一次更新中变化的部分(通过diff算法),因组件的render函数在该阶段调用而得名。

render阶段「可能」是异步的(取决于触发更新的场景)。

  • commit阶段

commit阶段会将render阶段计算的需要变化的部分渲染在视图中。对应ReactDOM来说会执行appendChildremoveChild等。

commit阶段一定是同步调用(这样用户不会看到渲染不完全的UI

我们通过ReactDOM.render创建的应用属于legacy模式。

在该模式下一次render阶段对应一次commit阶段。

如果我们通过ReactDOM.createRoot(当前稳定版本中还没有此API)创建的应用属于开篇提到的CMconcurrent模式)

CM下,更新有了优先级的概念,render阶段可能被高优先级的更新打断。

所以render阶段可能会重复多次(被打断后重新开始)。

可能多次render阶段对应一次commit阶段。

此外,还有个blocking模式用于方便开发者慢慢从legacy模式过渡到CM

你可以从特性对比看到不同模式支持的特性:

不同模式支持的特性

为什么需要CM?

知道了CM是什么,那么他有什么用?为什么React核心团队会耗时3年多(18年开始)来实现他?

这得从React的设计理念聊起。

我们可以从官网React哲学看到React的设计理念:

我们认为,React是用JavaScript构建「快速响应」的大型Web应用程序的首选方式。

其中「快速响应」是重点。

那么什么影响「快速响应」呢?React团队给出的答案:

CPU的瓶颈和IO的瓶颈

CPU的瓶颈

考虑如下demo,我们渲染3000的列表项:

代码语言:javascript
复制
function App() {
  const len = 3000;
  return (
    <ul>
      {Array(len).fill(0).map((_, i) => <li>{i}</li>)}
    </ul>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);  

刚才说过,在legacy模式下render阶段不会被打断,则这3000个lirender都得在同一个浏览器宏任务中完成。

长时间的计算会阻塞线程,造成页面掉帧,这就是CPU的瓶颈。

解决的办法就是:启用CM,将render阶段变为「可中断」的,

当浏览器一帧剩余时间不多时将控制权交给浏览器。等下一帧的空余时间再继续组件render

IO的瓶颈

除了长时间计算导致的卡顿,网络请求时的loading状态也会造成页面不可交互,这就是IO的瓶颈。

IO瓶颈是客观存在的。

作为前端,能做的只能是尽早请求需要的数据。

但是,通常情况下:「代码可维护性」「请求效率」是相悖的。

什么意思呢,举个例子:

假设我们封装了请求数据的方法useFetch,通过返回值是否存在区分是否请求到数据。

代码语言:javascript
复制
function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}

为了提高「代码可维护性」useFetch与要渲染的组件User存在于同一个组件App中。

然而,如果User组件内还需要进一步请求数据呢(如下profile数据)?

代码语言:javascript
复制
function User({data}) {
  const {id, name} = data?.id || {};
  const profile = useFetch(id);
  
  return (
    <div>
      <p>{name}</p>
      {profile ? <Profile data={profile} /> : null}
    </div>
  )
}

本着「代码可维护性」原则,useFetch与要渲染的组件Profile存在于同一个组件User中。

但是,这样组织代码,Profile组件只能等User render后再render

数据只能像瀑布的水一样,一层一层流下来。

这种低效的请求数据方式被称为waterfall

为了提高「请求效率」,我们可以将“请求Profile组件所需数据的操作”提到App组件内,合并在useFetch中:

代码语言:javascript
复制
function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}

但是这样就降低了「代码可维护性」Profile组件离profile数据太远)。

React团队从Relay团队借鉴经验,借助Suspense特性,提出了Server Components。

就是为了在处理IO瓶颈时兼顾「代码可维护性」「请求效率」

这一特性的实现需要CM「更新有不同优先级」

CM为什么花费这么久?

接下来,我们从源码特性生态三个方面,自底向上看看CM的普及有多么不容易。

源码层面

优先级算法改造

在v16.13之前,React已经实现了基本的CM功能。

我们之前聊过,CM有更新优先级的概念。之前是通过一个毫秒数expirationTime标记「更新」的过期时间。

  • 通过对比不同更新的expirationTime判断优先级高低
  • 通过对比更新的expirationTime与当前时间判断更新是否过期(过期需要同步执行)

但是,expirationTime作为一个与时间相关的浮点数,无法表示「一批优先级」这个概念。

为了实现更上层的Server Components特性,需要有「一批优先级」这个概念。

于是,核心成员「Andrew Clark」开始了旷日持久的优先级算法改造,见:PR lanes

Offscreen支持

在此同时,另一个成员「Luna Ruan」在开发一个新API —— Offscreen

可以理解这是React版的Keep-Alive特性。

订阅外部源

未开启CM前,在一次更新如下三个生命周期只会调用一次:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

但是开启CM后,由于render阶段可能被打断、重复,所以他们可能被调用多次。

在订阅外部源(比如注册事件回调)时,可能更新不及时或者内存泄漏。

举个例子:bindEvent是一个基于「发布订阅」的外部依赖(比如一个原生DOM事件):

代码语言:javascript
复制
class App {
  componentWillMount() {
    bindEvent('eventA', data => {
      thie.setState({data});
    });
  }
  componentWillUnmount() {
    bindEvent('eventA');
  }
  render() {
    return <Card data={this.state.data}/>;
  }
}

componentWillMount中绑定,在componentWillUnmount中解绑。

当接收到事件后,更新data

render阶段反复中断、暂停后,有可能出现:

事件最终绑定前(bindEvent执行前),事件源触发了事件

此时App组件还未注册该事件(bindEvent还未执行),那么App获取的data就是旧的。

为了解决这个潜在问题,核心成员「Brian Vaughn」开发了特性:create-subscription

用来在React中规范外部源的订阅与更新。

简单说就是将外部源的注册与更新在commit阶段与组件的状态更新机制绑定上。

特性层面

「源码层面」的支持完备后,基于CM的新特性开发便提上日程。

这便是Suspense

[Umbrella] Releasing Suspense #13206,这个PR负责记录Suspense特性的进展。

Umbrella标记代表这个PR会影响非常多库、组件、工具

可以看到,长长的时间线从18年一直到最近几天。

最初Suspense只是「前端特性」,当时React SSR只能向前端传递「字符串」数据(也就是俗称的脱水

后来React实现了一套SSR时的组件「流式」传输协议,可以「流式」传输组件,而不仅仅是HTML字符串。

此时,Suspense被赋予更多职责。也拥有了更复杂的优先级,这也是刚才讲过的「优先级算法改造」的一大原因。

最终的成果,就是今年早些时候推出的Server Components概念。

生态层面

「源码层面」支持了、「特性」也开发完成了,是不是就能无缝接入呢?

还早。

作为一艘行驶了8年的巨轮,React每次升级到最终社区普及,中间都有巨量的工作要做。

为了帮助社区慢慢过渡到CMReact做了如下工作:

  • 开发ScrictMode特性,并且是默认启用的,规范开发者写法
  • componentWillXXX标记为unsafe,提醒用户不要使用,未来会废弃
  • 提出了新生命周期(getDerivedStateFromPropsgetSnapshotBeforeUpdate)替代如上将被废弃的生命周期
  • 开发了legacy模式与CM过渡的中间模式 —— blocking模式

而这,只是过渡过程中「最简单」的部分。

难的部分是:

社区当前积累的大量基于legacy模式的库如何迁移?

很多动画库、状态管理库(比如mobX)的迁移并不简单。

总结

我们介绍了CM的来龙去脉以及他迁移的难点。

通过这篇文章,想必你也知道了开头那个为React增加createRoot(开启CM的方法)是多么不容易。

好在一切都是值得的,如果说以前React的壁垒在于:开源时间早、社区规模大。

那么从CM开始,React 「可能」会是前端领域最复杂的视图框架。

届时,不会有任何一个React-like的框架能实现React同样的feature

但是也有人说,CM带来的这些功能就是鸡肋,我根本不需要。

你觉得CM怎么样?欢迎留下你的讨论。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 魔术师卡颂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CM是什么
  • 为什么需要CM?
    • CPU的瓶颈
      • IO的瓶颈
      • CM为什么花费这么久?
        • 源码层面
          • 优先级算法改造
        • Offscreen支持
          • 订阅外部源
            • 特性层面
            • 生态层面
            • 总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档