Skip to content

react hydrate 模式 #39

@creamidea

Description

@creamidea

快速预览

react hydrate 是指,在 SSR 模式下渲染出 html 字符串发送到浏览器渲染之后,绑定监听事件以及将 react Fiber 树构建出来的过程,也就是所谓的「水合」,或者叫做「注水」。

一个简单的例子🌰

import { createElement } from "react";
import {
  renderToString,
} from "react-dom/server";

import http from "http";

const hello = createElement(
  "div",
  {
    onClick: () => {
      console.log("hello");
    },
  },
  "Hello World"
)

const server = http.createServer((req, res) => {
  const html = renderToString(
    hello
  );

  res.end(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
        <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    </head>
    <body>
        <div id="root">${html}</div>
        <script>
          const { createElement } = window.React;
          const { hydrateRoot, createRoot } = window.ReactDOM;
          const root = hydrateRoot(
            document.getElementById('root'),
            createElement(
              "div",
              {
                onClick: () => {
                  console.log("hello");
                },
              },
              "Hello World"
            ),
          );
        </script>
    </body>
    </html>`);
});

server.listen(3001);

大致过程:通过 renderToString 等方法,将 React 组件代码处理成 HTML(Fiber 树 => HTML,去掉所有绑定事件,只留 HTML 信息),返回浏览器或者搜索引擎,实现 SSR 的效果。客户端 React 也不需要再一次处理 DOM 元素,只需要绑定的事件,处理 Fiber 树等即可。一举两得。

重点就在下面的代码里面,调用 hydrateRoot 时第二个参数,也就是把 React 组件重新「执行」一下。这里的 React 组件要和服务端 SSR 时放入的组件一致。

<script>
  const { createElement } = window.React;
  const { hydrateRoot, createRoot } = window.ReactDOM;
  const root = hydrateRoot(
    document.getElementById('root'),
    createElement(
      "div",
      {
        onClick: () => {
          console.log("hello");
        },
      },
      "Hello World"
    ),
  );
</script>

下面进入代码分析阶段,回答 2 个问题:

  1. 事件什么时候绑定
  2. Fiber 树如何被构建出来,HostComponent 的 fiber.stateNode 何时被赋值?

事件什么时候绑定

hydrateRoot 函数内部会调用 listenToAllSupportedEvents(container),这里的 container 就是 DOM 容器节点 。listenToAllSupportedEvents 函数就是处理委托事件的开始入口,具体实现可以看这篇文章

Fiber 树构建

承接上段,hydrateRoot 函数内部会执行如下创建逻辑,进入 Fiber 树构造。最大的区别在于,跳过 DOM 树的创建和增加。

  • createHydrationContainer (createFiberRoot: FiberRootNode, createHostRootFiber)
    createFiberRoot 的第 3 个参数被赋值为 true,最终影响该数据结构
const initialState: RootState = {
  element: initialChildren,
  isDehydrated: hydrate,
  cache: (null: any), // not enabled yet
  transitions: null,
};
uninitializedFiber.memoizedState = initialState;
  • scheduleInitialHydrationOnRoot,和 scheduleUpdateOnFiber 相比,代码更加简单,标记 lanes,就直接进入 ensureRootIsScheduled 阶段
export function scheduleInitialHydrationOnRoot(
  root: FiberRoot,
  lane: Lane,
  eventTime: number,
) {
  const current = root.current;
  current.lanes = lane;
  markRootUpdated(root, lane, eventTime);
  ensureRootIsScheduled(root, eventTime);
}

以优先级 16 开启更新

开始更新时,使用默认优先级 16,React18 版本下会进入并发,但不会分片。(16 是在 BlockingLane 范围内)

Render 阶段

updateHostRoot

在 render 阶段的 beginWork 阶段处理 HostRoot 的 updateHostRoot 函数内会对 isDehydrated 判断。如果是真值,会更新 updateQueue.baseState 和 workInProgress.memoizedState

// Flip isDehydrated to false to indicate that when this render
// finishes, the root will no longer be dehydrated.
const overrideState: RootState = {
  element: nextChildren,
  isDehydrated: false,
  cache: nextState.cache,
  transitions: nextState.transitions,
};
const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
// `baseState` can always be the last state because the root doesn't
// have reducer functions so it doesn't need rebasing.
updateQueue.baseState = overrideState;
workInProgress.memoizedState = overrideState;

以及调用 enterHydrationState 函数:获取第一个 DOM 节点并赋值全局变量 nextHydratableInstance

enterHydrationState(workInProgress);

const parentInstance: Container = fiber.stateNode.containerInfo;
nextHydratableInstance = getFirstHydratableChildWithinContainer(
	parentInstance,
);

updateHostComponent

在 render 阶段的 beginWork 阶段处理 HostComponent 的 updateHostComponent 函数,会将已经存在的 DOM 实例和子 fiber 进行关联,也就是赋值 fiber.stateNode,通过 tryToClaimNextHydratableInstance 完成,tryHydrate 函数完成具体的赋值 stateNode 操作。

在 render 阶段的 completeOfWork 内处理 HostComponent 分支时,下面的语句返回真值,进入「水合」处理,判断当前节点是否存在更新

var _wasHydrated = popHydrationState(workInProgress);
if (_wasHydrated) {
  if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
    markUpdate(workInProgress);
  }
} else {
...
}

Commit 阶段

HostRoot

根据 HostRoot 的 memoizedState.isDehydrated 为 true,会进入 commitHydratedContainer 逻辑,该逻辑会处理 queuedDiscreteEvents。

后续

处理所有副作用和生命周期,和正常的 Commit 阶段类似,不再赘述。


以上

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions