🕌

ReactのLaneとはなにか

2021/06/18に公開

React内部で「Lane」という仕組みが用いられている。

Initial Lanes implementation by acdlite · Pull Request #18796 · facebook/react

先日React 18 alphaが発表されたが、Concurrentな機能の多くはLaneの機構をベースとしてつくられている。

Laneは、Reconcile時のタスクを32bitで表す。Laneを表す実際のコードは、例えば以下のようになる。

// https://github.com/facebook/react/blob/9212d994ba939f20a04220a61e9776b488381596/packages/react-reconciler/src/ReactFiberLane.new.js
const NoLane: Lane = 0b0000000000000000000000000000000;
const SyncLane: Lane = 0b0000000000000000000000000000001
const TransitionLanes: Lanes = 0b0000000001111111111111111000000;
const IdleLane: Lanes = 0b0100000000000000000000000000000;
const OffscreenLane: Lane = 0b1000000000000000000000000000000;

このように、タスクの種別ごとにLaneが存在する。NoLane(タスクがないとき)は例外だが、基本的には優先度の高いLaneほど小さな数値で表されている。

32bitで表すことで、Laneの操作にBitmaskを使うことができる。例えば、複数のLaneをひとつのLaneにBitmaskすれば、Laneすべてを相対的に比較する必要がなくなり、実装もシンプルに、メモリも節約できる。

実際にBitmaskを行う関数を見てみる。

export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

この関数は名前のとおりだが、Laneをマージして返す関数だ。OR演算を行うことでLaneをマージする。例えば、以下のように使える。

mergeLanes(
  NoLane /*0b0000000000000000000000000000000*/,
  OffscreenLane /*0b1000000000000000000000000000000*/
)

// => 0b1000000000000000000000000000000

上記の例では、タスクがないLane(NoLane)をOffscreenLaneに更新している。LaneはFiberに紐付けられているので、以下のように、対象となるFiberのLaneを更新することができる。

fiber.lanes = mergeLanes(
  fiber.lanes /* NoLane */, 
  OffscreenLane
)
// => OffscreenLane

もうひとつ isSubsetOfLanes という関数を見てみる。

export function isSubsetOfLanes(set: Lanes, subset: Lanes) {
  return (set & subset) === subset;
}

この関数は、LaneのAND演算を行った結果がsubsetに一致するかどうかを返している。これだけだとなにが嬉しいのかよくわからないので具体的なパターンをいくつか書いてみる。

isSubsetOfLanes(
  NonIdleLanes, /*0b0001111111111111111111111111111*/
  SyncLane /*0b0000000000000000000000000000001*/
)
// => true. SyncLane は Idle なタスクではない

isSubsetOfLanes(
  NonIdleLanes, /*0b0001111111111111111111111111111*/
  OffscreenLane /*0b1000000000000000000000000000000*/
)
// => false. OffscreenLane は Idle なタスク

isSubsetOfLanes(
  TransitionLanes, /*0b0000000001111111111111111000000*/
  TransitionLane1 /*0b0000000000000000000000001000000*/
)
// => true. TransitionLane1 は TransitionLanes に含まれる

上記のように、 isSubsetOfLanes では、該当のLaneが対象のFiberの部分集合であるか?というのを判別できる。

例えば、 scheduleWorkOnParentPath という関数がある。この関数は、ざっくりいうと、「下位のchildrenにタスクがある」ということを上位のparentに知らせてあげる役割を持つ。

// https://github.com/facebook/react/blob/a8964649bb6332cf1f8d723f81ce97cc5a1886ff/packages/react-reconciler/src/ReactFiberNewContext.new.js#L142
export function scheduleWorkOnParentPath(
  parent: Fiber | null,
  renderLanes: Lanes,
) {
  // Update the child lanes of all the ancestors, including the alternates.
  let node = parent;
  while (node !== null) {
    const alternate = node.alternate;
    if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
      node.childLanes = mergeLanes(node.childLanes, renderLanes);
      if (alternate !== null) {
        alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
      }
    } else if (
      alternate !== null &&
      !isSubsetOfLanes(alternate.childLanes, renderLanes)
    ) {
      alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
    } else {
      // Neither alternate was updated, which means the rest of the
      // ancestor path already has sufficient priority.
      break;
    }
    node = node.return;
  }
}

ここで node.return はparent、もしくは複数のparentのFiberなので、parentのパスを順にたどり、childLanesを更新してゆく関数だということがわかる。例えば、childrenでReact.Contextが更新されたときに、parentにそのことを伝えるときなどに使われる。

この関数の中で isSubsetOfLanes が使われている。

    if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
      node.childLanes = mergeLanes(node.childLanes, renderLanes);
      if (alternate !== null) {
        alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
      }
    } else if (

nodeはここではparentを表しているので、ここでは、「parent.childLanes (コード上は node.childLanes ) が、対象のLaneの部分集合でなければ、parent.childLanesを、対象のLaneとマージした値に更新する」ということをしているというのがわかる。こうすることで、childrenのLaneをparent側に移すことができる。結果として、Reconcile時にfiber.childrenLanesを見れば、下位層に再レンダリングが必要なことがわかる、という寸法である。

このように、Laneのおかげで複数のタスクをまとめあげて処理をするのが容易になるし、それでいて優先度を少ないパスで判別できる。Reconcile時にはLaneを参照したり更新したりマージしたりすればよく、本来のアルゴリズムに集中でき、結果として、FiberやSuspenseの考え方にマッチするアーキテクチャが実現できるのだ。

React 18 alphaで実装される機能には、Lane以外にもコアとなるPRがいくつかあって、それは下記のスレッドが詳しい。

https://twitter.com/rickhanlonii/status/1402771549808214016

https://twitter.com/dan_abramov/status/1402927593406582787

https://twitter.com/acdlite/status/1402982843962343425

Fiberが発案されてからここまで来るのに設計、実装、検証、設計を繰り返していよいよメジャーバージョンアップがされたと思うと、すごくないですか?自分は神様のバレーくらい感動しました。

Discussion