Day29-浅谈 React Concurrent Mode & 相关功能(Fiber、Suspense、useTransition、useDeferredValue)

这篇会介绍 React 的一个新模式 Concurrent Mode 以及关於它的一些功能。

Concurrent Mode

Concurrent Mode 是什麽?

它由数个新的 React 功能组成,不过这些功能都还在实验阶段。

目前 React 有三种模式,由图可以看到原本的 React 程序码变成了 Legacy Mode,另外还有夹在两个模式过渡的 Blocking Mode。在 Concurrent Mode 的部分,新增了几个新的功能/特性,本文将会介绍标题提到的那几项功能。

图片来源

使用 Concurrent Mode

如果想要先试用这个新功能的话,需先安装:

npm install react@experimental react-dom@experimental

原本在根元件的程序也要修改:

import ReactDOM from 'react-dom';
const root = document.getElementById('root');

// 旧
ReactDOM.render(<App />, root);

// 新
ReactDOM.createRoot(root).render(<App />);

参考资料

REACT CONCURRENT MODE

Concurrent Mode API Reference (Experimental)

Fiber

复习 Reconciliation

在了解 Fiber 这个东西前,先简单复习一下 Reconciliation。

在 React 中因为状态的改变而要重新渲染时,一连串决定哪些 DOM 元素要做更新的过程,就是 Reconciliation。包含以下的步骤:

  1. React 读取在网页上的 DOM Tree 并复制一份变成 Virtual DOM 储存起来
  2. 当网页上某元件的 DOM 有任何改变时,会产生新的一组 Virtual DOM
  3. 透过 Diffing 演算法,在记忆体内部算出要更新的 Virtual DOM
  4. 最後在网页上的 Real DOM 只会更新需要更新的 DOM

Fiber 的由来

一个功能的产生想必是为了解决某个问题,Fiber 也是如此,所以先了解一下在 Fiber 出现前的 React 有什麽问题?

在 React V15 中的 React,使用的是 Stack reconciler 去比对更新前後的 DOM Tree 哪里不一样。

当进行 Reconciliation 时,比对两个 Virtual DOM Tree 不同的部分是透过从 DOM Tree 的根节点不断做递回将整个 DOM Tree,并搭配上 call stack(堆叠)的方式和另一个 DOM Tree 做比对,以找出不同之处。

若 Tree 的结构较为复杂的话,运作这些步骤将会很花时间,来不及在一次 frame 时间内处理完时就会掉帧。

Fiber 便是为了解决效能和渲染时的流程度而生。

Fiber 功用

在 React V16 中,Fiber reconciler 取代了 Stack reconciler,这个东西可以根据整个要渲染的 DOM Tree 创造出多个小节点,将整个渲染的任务拆分成许多的小任务,也就是这次要介绍的主角: Fiber。

由 Fiber 构成的渲染任务可以被中断、重用、舍弃,并且不同的渲染任务可以被设定优先权做渲染。

而单独的一个 Fiber 有点类似 React element 的概念,会对应到 Virtual DOM 的元素,并且 Fiber 也能储存一些元件的资料和状态(props、state、updateQueue...),关於 Fiber 相关的属性可以参考以下 React 的原始码:

FiberNode

很多个 Fiber 会构成一个 Fiber Tree,其资料结构为 Linked List,以下是简单的范例:

另外推荐一个网页,呈现了用 Stack reconciler 和 Fiber reconciler 两个方式频繁改变 Sierpinski triangle 内容的差异,可以看到 Stack reconciler 的方式会造成明显的卡顿,Fiber reconciler 则不会。

Fiber vs Stack Demo

Fiber 和渲染阶段

Day23 的文章我有提到在 React 渲染时,会分成 Render Phase & Commit Phase,在这两个阶段时,Fiber 也会进行一些事情。

Render Phase

在此阶段 Fiber reconciler 会建立 Fiber,产生 Fiber Tree,,若有需要更新的 DOM,会再建立出一个新的 Fiber Tree,称为 workInProgress Tree。

Commit Phase

在此阶段,会为 Fiber 执行在它们上面记录的 side effect,并且在此阶段运行时不可以中断。

当 workInProgress Tree 完成更新时,会变成新的 Fiber Tree,等下次更新时又会出现新的 orkInProgress Tree,如此循环下去,这样的过程称为 Double Buffering。

Fiber 对生命周期的影响

在 React V16 中,废弃了 componentWillMount、componentWillUpdate、componentWillReceiveProps,这点在 Day22 有稍微提到,而这些生命周期函式被废弃就和 Fiber 的出现有所关联。

由於这些函式都出现在 Render 阶段,在前面说到渲染任务可以被中断、重用、舍弃,并且不同的渲染任务可以被设定优先权做渲染,所以这些废弃的生命周期函式都可能被重复执行,

参考资料 & 推荐阅读

以下的影片和文章都解说的蛮详细的,若有兴趣更加了解的读者可以进一步阅读。

What Is React Fiber?

React Fiber 浅谈

fiber reconciler 漫谈

(译)深入了解React Fiber的内部


useTransition

常和 Suspense 搭配使用,它可以延迟渲染指定的元件,让优先度较低或是比较要耗时的元件稍後渲染。

另外如果 loading 状态过短,使用者可能在网页上看到载入图示只有一瞬间的时间,那这样其实也不太需要 Loading 元件,此时透过 useTransition 延时也可以避免不必要的 Loading 元件呈现在网页。

语法

const [startTransition, isPending] = useTransition({ timeoutMs: 指定的时间 });

  • useTransition 接收一个物件当作参数,用来设定延迟的时间
  • 回传了名为 startTransition 的 callback 函式,
  • 回传判断 transition 状态是否完成的 boolean 值: isPending。

使用

程序码来自官网范例,在这段程序码中,点击按钮时会去取得 profile 的资料,但因为将设定取回资料的 setResource 放在了 startTransition 里面,所以会延迟更新 state,而在更新 state 前,会先出现 isPending 条件式的内容。

function App() {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition({ timeoutMs: 2000 });
  return (
    <>
      <button
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId));
          });
        }}
      >
        Next
      </button>
      {isPending ? " Loading..." : null}
      <Suspense fallback={<Spinner />}>
        <ProfilePage resource={resource} />
      </Suspense>
    </>
  );
}

useDeferredValue

语法

const deferredValue = useDeferredValue(value, { timeoutMs: 指定的时间 });

  • 第一个参数是设定要延迟的 value 值
  • 第二个参数是延迟的秒数
  • 最後返回一个延迟的值 deferredValue

使用

程序码来自官网范例,input 输入框的值会随着使用者打字不断快速改变,但 MySlowList 会等待两秒後才会进行更新。

function App() {
  const [text, setText] = useState("hello");
  const deferredText = useDeferredValue(text, { timeoutMs: 2000 }); 

  return (
    <div className="App">
      {/* Keep passing the current text to the input */}
      <input value={text} onChange={handleChange} />
      ...
      {/* But the list is allowed to "lag behind" when necessary */}
      <MySlowList text={deferredText} />
    </div>
  );
 }

Suspense

Suspense 在之前的 Day24 时就有介绍过,不过在 Concurrent Mode 出现之前,就有这个元件了,但只有动态载入的功能,而在 Concurrent Mode 中,增加了可以加入 Loading 状态的元件,资料持续载入的期间就先暂时渲染 Loading 元件。


终於只剩最後一天的铁人赛!明天会整理一些不错的 youtube react 学习资源清单给读者做更多的延伸学习和参考~


<<:  Day28 Session 的使用-1

>>:  # Day28--让commit像战国时代一样分分合合

[Day 1] 主角总是最後登场的 (後端篇)

其实只是拖延症点到满等的我,说是主角其实只是拖延症发作 我通常都是先撰写前端篇才写後端篇,所以看官们...

Day 1 序言及基本运算元件

我很早就开始接触组合语言,但没有学太久,就没有再碰了,当初学组合语言的原因,是觉得组合语言是人与机器...

Day 13 懒得想变数吗? RSpec 有提供你啦

该文章同步发布於:我的部落格 还记得我们使用 let 方法来实作一个物件来让我们可以快速使用! 但...

连续 30 天 玩玩看 ProtoPie - Day 7

怎麽有东西挡在前面 昨天有提到为什麽点击 icon 放大之後,还是有一些其他 icon 在上面呢? ...

30天零负担轻松学会制作APP介面及设计【DAY 23】

大家好,我是YIYI,今天要来分享我设计的APP的PHOTOTYPE制做过程。 今日进度 今天的进度...