前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React Concurrent Mode三连:是什么/为什么/怎么做

React Concurrent Mode三连:是什么/为什么/怎么做

作者头像
公众号@魔术师卡颂
发布2020-11-10 11:31:46
2.4K0
发布2020-11-10 11:31:46
举报
文章被收录于专栏:魔术师卡颂魔术师卡颂

最近发布的React v17.0没有包含新特性。

究其原因,v17.0主要的工作在于源码内部对Concurrent Mode的支持。所以v17版本也被称为“垫脚石”版本。

本文会详细介绍Concurrent Mode的来龙去脉,以及这套体系从底层架构到上层API的实现。

由于跨度比较长,细节难免缺失。对文中提到的细节的进一步补足,欢迎关注我的工粽号 —— 魔术师卡颂,给你一份完整的源码学习方案。

是什么?

Concurrent Mode是什么?你可以从官网Concurrent 模式介绍[1]了解其基本概念。

一句话概括:

Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。

为了让应用保持响应,我们需要先了解是什么在制约应用保持响应

我们日常使用App,浏览网页时,有两类场景会制约保持响应

  • 当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。
  • 发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

这两类场景可以概括为:

  • CPU的瓶颈
  • IO的瓶颈

CPU的瓶颈

当项目变得庞大、组件数量繁多时,就容易遇到CPU的瓶颈。

考虑如下Demo,我们向视图中渲染3000个li

代码语言: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);  

主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。

我们知道,JS可以操作DOM,GUI渲染线程JS线程是互斥的。所以JS脚本执行浏览器布局、绘制不能同时执行。

在每16.6ms时间内,需要完成如下工作:

代码语言:javascript
复制
JS脚本执行 -----  样式布局 ----- 样式绘制

当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局样式绘制了。

在Demo中,由于组件数量繁多(3000个),JS脚本执行时间过长,页面掉帧,造成卡顿。

可以从打印的执行堆栈图看到,JS执行时间为73.65ms,远远多于一帧的时间。

如何解决这个问题呢?

答案是:在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件(可以看到,在源码[2]中,预留的初始时间是5ms)。

当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作。

这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)

所以,解决CPU瓶颈的关键是实现时间切片,而时间切片的关键是:将同步的更新变为可中断的异步更新

IO的瓶颈

网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下,减少用户对网络延迟的感知?

React给出的答案是将人机交互研究的结果整合到真实的 UI 中[3]

这里我们以业界人机交互最顶尖的苹果举例,在IOS系统中:

点击“设置”面板中的“通用”,进入“通用”界面:

作为对比,再点击“设置”面板中的“Siri与搜索”,进入“Siri与搜索”界面:

你能感受到两者体验上的区别么?

事实上,点击“通用”后的交互是同步的,直接显示后续界面。

而点击“Siri与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面。

但从用户感知来看,这两者的区别微乎其微。

这里的窍门在于:点击“Siri与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。

当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示loading的效果。

试想如果我们一点击“Siri与搜索”就显示loading效果,即使数据请求时间很短,loading效果一闪而过。用户也是可以感知到的。

为此,React实现了Suspense[4]useDeferredValue[5]

在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新

Concurrent Mode自底向上

底层基础决定了上层API的实现,接下来让我们了解下,Concurrent Mode自底向上都包含哪些组成部分,才能实现上文提到的功能。

底层架构 —— Fiber架构

从上文我们了解到,为了解决CPU、IO瓶颈,最关键的一点是:实现异步可中断的更新。

基于这个前提,React花费2年时间重构完成了Fiber架构。

Fiber机构的意义在于,他将单个组件作为工作单元,使以组件为粒度的“异步可中断的更新”成为可能。

架构的驱动力 —— Scheduler

如果我们同步运行Fiber架构(通过ReactDOM.render),则Fiber架构与重构前并无区别。

但是当我们配合时间切片,就能根据宿主环境性能,为每个工作单元分配一个可运行时间,实现“异步可中断的更新”。

于是,scheduler[6](调度器)产生了。

Scheduler能保证我们的长任务被拆分到每一帧不同的task中。

当我们为上文讲到的渲染3000个li的Demo开启Concurrent Mode

代码语言:javascript
复制
// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);  
ReactDOM.unstable_createRoot(rootEl).render(<App/>);

可以看到,每段JS脚本执行时间大体在5ms左右。

这样浏览器就有剩余时间执行样式布局样式绘制,减少掉帧的可能性。

Fiber架构配合Scheduler实现了Concurrent Mode的底层刚需 —— “异步可中断的更新”。

架构运行策略 —— lane模型

到目前为止,通过SchedulerReact可以控制更新Fiber架构中运行/中断/继续运行。

基于当前的架构,当一次更新在运行过程中被中断,过段时间再继续运行,这就是“异步可中断的更新”。

当一次更新在运行过程中被中断,转而重新开始一次新的更新,我们可以说:后一次更新打断了前一次更新

这就是优先级的概念:后一次更新优先级更高,他打断了正在进行的前一次更新

多个优先级之间如何互相打断?优先级能否升降?本次更新应该赋予什么优先级

这就需要一个模型控制不同优先级之间的关系与行为,于是lane模型诞生了。

lane模型通过将不同优先级赋值给一个位,通过31位的位运算来操作优先级

如下是不同优先级的定义:

代码语言:javascript
复制
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;

// 省略...

上层实现

现在,我们可以说:

从源码层面讲,Concurrent Mode是一套可控的“多优先级更新架构”。

那么基于该架构之上可以实现哪些有意思的功能?我们举几个例子:

batchedUpdates

如果我们在一次事件回调中触发多次更新,他们会被合并为一次更新进行处理。

如下代码执行只会触发一次更新

代码语言:javascript
复制
onClick() {
  this.setState({stateA: 1});
  this.setState({stateB: false});
  this.setState({stateA: 2});
}

这种合并多个更新的优化方式被称为batchedUpdates

batchedUpdates在很早的版本就存在了,不过之前的实现局限很多(脱离当前上下文环境的更新不会被合并)。

Concurrent Mode中,是以优先级为依据对更新进行合并的,使用范围更广。

Suspense

Suspense[7]可以在组件请求数据时展示一个pending状态。请求成功后渲染数据。

本质上讲Suspense内的组件子树比组件树的其他部分拥有更低的优先级

useDeferredValue

useDeferredValue[8]返回一个延迟响应的值,该值可能“延后”的最长时间为timeoutMs

例子:

代码语言:javascript
复制
const deferredValue = useDeferredValue(value, { timeoutMs: 2000 });

useDeferredValue内部会调用useState并触发一次更新

这次更新优先级很低,所以当前如果有正在进行中的更新,不会受useDeferredValue产生的更新影响。所以useDeferredValue能够返回延迟的值。

当超过timeoutMsuseDeferredValue产生的更新还没进行(由于优先级太低一直被打断),则会再触发一次高优先级更新

总结

除了以上介绍的实现,可以预见,当v17完美支持Concurrent Mode后,v18会迎来一大波基于Concurrent Mode的库。

参考资料

[1]

Concurrent 模式介绍: https://zh-hans.reactjs.org/docs/concurrent-mode-intro.html

[2]

源码: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L119

[3]

将人机交互研究的结果整合到真实的 UI 中: https://zh-hans.reactjs.org/docs/concurrent-mode-intro.html#putting-research-into-production

[4]

Suspense: https://zh-hans.reactjs.org/docs/concurrent-mode-suspense.html

[5]

useDeferredValue: https://zh-hans.reactjs.org/docs/concurrent-mode-reference.html#usedeferredvalue

[6]

scheduler: https://github.com/facebook/react/tree/master/packages/scheduler

[7]

Suspense: https://zh-hans.reactjs.org/docs/concurrent-mode-suspense.html

[8]

useDeferredValue: https://zh-hans.reactjs.org/docs/concurrent-mode-reference.html#usedeferredvalue

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 是什么?
  • CPU的瓶颈
  • IO的瓶颈
  • Concurrent Mode自底向上
    • 底层架构 —— Fiber架构
      • 架构的驱动力 —— Scheduler
        • 架构运行策略 —— lane模型
          • 上层实现
            • batchedUpdates
            • Suspense
            • useDeferredValue
          • 总结
            • 参考资料
        相关产品与服务
        语音识别
        腾讯云语音识别(Automatic Speech Recognition,ASR)是将语音转化成文字的PaaS产品,为企业提供精准而极具性价比的识别服务。被微信、王者荣耀、腾讯视频等大量业务使用,适用于录音质检、会议实时转写、语音输入法等多个场景。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档