大话React18,从Fiber到Concurrent Mode

写在前面

距离上次有存在感的React更新(v16.8)已经过去三年多了,那个时候React Hooks也成为了风靡一时的知识,直到现在,几乎以及完全替代了传统的类组件,期间React又更新了v17版本,但17版本对于开发者来说,基本也是存在感很低,因为对现有的开发没有任何影响,基本上是底层的更新。直到今年的 3 月 29 日,React18迎来了更新,这次更新可谓是“十年磨一剑”,甚至说,之前所有的版本更新,都是为了React18做准备。那么接下来,就跟着我的脚步一起了解React18吧!

React发展史

这里为了照顾新手读者,我们需要介绍一下React的历史,让一些来的不是那么突兀;如果你是老手React开发,那么你可以绕过这一段落。
React作为一个优秀的前端框架,有着很复杂的发展过程,这里我们简述几个对开发者比较有感知的历史
👉 V15,作为一个比较经典古老的版本,也为很多React开发者打下了基础,在V15版本中React组件创建方式是这样的:

  1. React.createClass(已废弃)
const App = React.createClass({
  render() {
    return <div>hello</div>
  }
})
  1. Class 组件(类组件、有状态组件)
class App extends React.component{
  render() {
    return <div>hello</div>
  }
}
  1. 函数组件(UI组件、傻瓜组件、无状态组件)
const App = () => {
  return <div>hello</>
}

另外,V15中底层更新采用的传统堆栈diff算法。
👉 V16, V16作为一个重要版本,算是最有存在感的一个版本,有以下几个开发者感触比较深的点;

  1. 加入了 React.Fragment Api,减少代码多余DOM
const App = () => {
  return (
    <React.Fragment> // 不会被渲染
        <div>hello</>
        <div>hello</>
    </React.Fragment>
  )
}
  1. 加入了memo Api,让函数组件也拥有了shouldComponentUpdate的能力
const App = (props) => {
  return <div {...props}>hello</>
}
export default React.memo(App)
  1. 加入hooks概念,让函数组件拥有了像类组件那样的能力并通过钩子的形式让函数组件具备各种能力扩展;
const App = (props) => {
  useEffect(() => {
    console.log("更新/挂载了")
  },[])
  return <div {...props}>hello</>
}
export default React.memo(App)

因为hooks内容比较多,且不在本文重点讨论范围内,这里不做赘述,需要了解更多,请移步我的历史文章《React Hooks,彻底颠覆React,它的未来应该是这样的》

  1. 废弃了一些生命周期,如:componentWillMountcomponentWillUpdate等,改用静态方法
  2. 虽然对用户无太大感知,但这里不得不提一下React Fiber。他将React推向了一个新的高度,取代了原先的堆栈式diff算法。也为后来的React 18以及后续发展奠定了基础,这里依然不多说,毕竟React Fiber要想讲清楚也是需要一个专门的话题,但简单总结一下 Fiber,那就是可中断的更新机制。我们用下面一个链接可以自行对比:
    https://claudiopro.github.io/react-fiber-vs-stack-demo/
    图中的数学模型叫“谢尔并斯基三角”,其特别就是节点无限增加,如果是传统的diff算法,那么在复杂的节点更新下,会出现肉眼可见的卡顿(频率小于60HZ)。但在Fiber 算法下,每一个节点便是一个 Fiber节点,其更新是可中断的。所以,看起来很柔和(刷新频率大于60HZ)。

👉 V17 React 可以说是作为一个过渡版本,大部分更新功能对于开发者没有感知,不过这里需要说一下比较重要的点,那就是事件代理机制更新了,我们都知道,React是合成事件,其中React17对事件机制做了调整,下面一幅图比较清楚


也就是说,在新版本的React中,事件不是冒泡到document中了,而是冒泡到我们的root根节点下。
另外,就是在React 17中试运行了 Concurrent Mode(并发模式),这里我们会在React18中详细介绍;在React18中,Concurrent Mode(并发模式)成了正式功能,这就是说,为什么React17是一个过渡版本;

React18功能一览

Concurrent Mode(并发模式)

正如上面提到的那样,并发模式在React17中已经被试用了,但直到React18才正式使用,下面我们简单来说一下;

CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI。

所以,他对于现有的功能以及生态不会有任何影响。

在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。



在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行

这里举个例子:有一天你上班正在划水,你打开了一个电影,这时候领导正朝你工位走来,在React18之前,你虽然心里很慌,但也只能等电影播放完才能打开你的编辑器继续工作,然后被领导一顿骂,但是在React18CM模式下,当你看到领导朝你的工位走来时,你意识到这是个紧急情况,于是你把电影关掉,打开个编辑器,躲过了领导。

不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化

但我们可以关注,基于CM实现的功能,这也是未来React18会一直深耕的东西

startTransition

上面提到CM对普通开发者,没有感知,但也有一些主动发挥其优势的案例,下面我们来说一下startTransition
React 的状态更新可以分为两类:

  • 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡,或者出问题了的感觉
  • 过渡更新(Transition updates):将 UI 从一个视图过渡到另一个视图。不需要即时响应,有些延迟是可以接受的。

CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。
因为React并不知道哪些是优先级更高的更新。看下面的代码demo

const [inputValue, setInputValue] = useState();
const [searchQuery, setSearchQuery] = useState();

const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  setSearchQuery(e.target.value);
}

return (
  <input value={inputValue} onChange={onChange} />
)

上面代码中,是通过在输入框中输入值,来改变searchQuery的值,并且注意到Input还是一个受控组件,也就是说,要有及时的状态响应。我们根据上面的定义不难分析出,值在Input组件中的及时反显是紧急的更新,而参数的更新是非紧急了,否则在极端情况下就会卡顿。
但是 React 确实没有能力自动识别。所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。
所以,在React18中,我们可以这样改造我们的代码

const [inputValue, setInputValue] = useState();
const [searchQuery, setSearchQuery] = useState();

const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  startTransition(() => { // 指定setSearchQuery为非紧急更新
     setSearchQuery(e.target.value);
  })
}

return (
  <input value={inputValue} onChange={onChange} />
)

下面我们用一个具体的Demo来演示上面讲到的


我们要操作一个毕达哥拉斯树,并且通过上面的<Slider/>组件来控制树的倾斜,这里我们先简单解释一下毕达哥拉斯树,其原始数学模型就像下面这张图

随着其对应角度的不同以及其层级的数量不同,树的倾斜程度和复杂度也不一样。因其复杂的特性,从页面渲染的角度来讲,他可以模拟比较极端的渲染场景。
我们还回到上面的倾斜度控制的demo中,其代码大概是这样的

const [treeLean, setTreeLean] = useState(0)

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLean(value)
}

return (
  <>
    <input type="range" value={treeLean} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

在每次 Slider 拖动后,React 执行流程大致如下:

  • 更新 treeLean
  • 渲染 input,填充新的 value
  • 重新渲染树组件 Pythagoras

但当树的节点足够多的时候,Pythagoras 渲染一次就非常慢,就会导致 Slider 的 value 回填变慢,用户感觉到严重的卡顿。如下图:



在 React 18 以前,我们是没有什么好的办法来解决这个问题的。但是上面我们提到,React18可以通过startTransition来区分紧急更新,在我们看来,表单的快速回填才是最紧急的,因为这里直接和用户的动作交互,相应不及时,就会有卡顿的现象,基于 React 18 CM 的可中断渲染机制,我们可以将树的更新渲染标记为低优先级的,就不会感觉到卡顿了。
我们这样改造代码

const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  // 将 treeLean 的更新用 startTransition 包裹,代表非紧急更新
  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

此时更新流程变为了
input 更新

  • treeLeanInput 状态变更
  • 准备新的 DOM
  • 渲染 DOM

树更新(这一次更新是低优先级的,随时可以被中止)

  • treeLean 状态变更
  • 准备新的 DOM
  • 渲染 DOM

React 会在高优先级更新渲染完成之后,才会启动低优先级更新渲染,并且低优先级渲染随时可被其它高优先级更新中断。
虽然我们降低了UI渲染的紧急性,但毕竟UI就变得响应不那么及时了,React 18 提供了 useTransition 来跟踪 transition 状态。我们可以设置一个loading来缓解这种UI上的加载卡顿,于是我们可以再次改造我们的代码


const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

// 实时监听 transition 状态
const [isPending, startTransition] = useTransition();

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Spin spinning={isPending}>
      <Pythagoras lean={treeLean} />
    </Spin>
  </>
)

自动批处理 Automatic Batching

批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如

function handleClick() {
  setCount(10);
  setFlag(false);
  // React 只会 re-render 一次,这就是批处理
}

在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会 render 两次,每次 state 变化更新一次
}, 1000);

而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。

function handleClick() { // 在回调事件中
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => { // 在setTimeOut中
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}, 1000);

如果你在某种场景下不想使用批处理,你可以通过 flushSync来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

OffScreen

该功能还未正式发布,不过可以简单描述下就是

“OffScreen 支持只保存组件的状态,而删除组件的 UI 部分。”

React开发者再也不用被说“React没有keep-alive了”当然OffScreen 不止是只有实现keep-alive这么简单

在 OffScreen 中,React 会保存住最后的状态,下次会用这些状态重新渲染组件。

看下面代码,

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  setPending(false)
}

在React18以前,如果在请求的过程中组件卸载了,那么就会报出一下错误

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

也就是说,你setPending(false)的时候,组件已经没有了,这就造成了内存泄漏。
而在React18,OffScreen中,就不会有这样的问题,因为如果你及时是在请求过程中卸载了组件,那组件的pengding状态依然是true
当然,这个功能目前还没有被发布,等正式发布了,我们再亲自实验这个问题吧!

新的hooks

  • useDeferredValue

用法:

const deferredValue = useDeferredValue(value);

useDeferredValue 可以让一个state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValuestartTransition 一样,都是标记了一次非紧急更新。

之前 startTransition 的例子,就可以用 useDeferredValue来实现。

const [treeLeanInput, setTreeLeanInput] = useState(0);

const deferredValue = useDeferredValue(treeLeanInput);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={deferredValue} />
  </>
)

写在后面

本文我们从React的历史到React18,在React18中主要围绕Concurrent Mode(并发模式)来进行了大量的讲解与实验。实际上,Concurrent Mode是React18中最核心的功能,在未来React会依赖此设计衍生出更多的功能,当然React18这次的更新不只有这些,但这里我们只介绍这么多
退一步讲,及时是React18更新了这么重要的东西,但对于普通开发者来说,我们可能并没有太多机会接触到,所以,也不要有太大的学习压力哦,总体来说,React18是为React未来打造的,对于现在的我们来说,他更新的功能可以说是无形的不过也希望读者朋友们能从本文中了解到关于React18的新知识

项目源码

这里我将本文中用到的演示demo代码放到了个人git仓库中,大家自取
https://github.com/sorryljt/react18-demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容