creamidea / creamidea.github.com

冰糖火箭筒&&蜂蜜甜甜圈
https://creamidea.github.io/
4 stars 4 forks source link

react hydrate 模式 #39

Open creamidea opened 2 years ago

creamidea commented 2 years ago

快速预览

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 树的创建和增加。

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 阶段类似,不再赘述。


以上