React 服务器渲染中,hydrateRoot 是核心,它将服务器段的渲染与客户端的交互绑定在一起,我们知道 React 中 Fiber Tree 是渲染的的核心,那么 React 是怎么实现 hydrateRoot 的呢?首先我们验证一下,hydrateRoot 和 createRoot 有什么区别。分别创建两个 React 组件,一个用 createRoot,一个用 hydrateRoot,为了演示方便,直接用引入 React 脚本的方式。
CreateRoot
本例中,页面 Dom已经存在了一个 Button 按钮,createRoot 会将其替换掉而不会重用。直接替换 Root 节点,从 Console 中可以看到返回为 false,节点不同。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"><button>0</button></div>
<script type="text/babel">
const useState = React.useState;
function App() {
const [state, setState] = useState(0);
return (
<button onClick={() => setState((state) => state + 1)}>
{state}
</button>
);
}
const rootElement = document.getElementById("root");
const originalButton = rootElement.firstChild;
ReactDOM.createRoot(rootElement).render(<App />);
setTimeout(
() =>
console.log(
originalButton === rootElement.firstChild
),
0
);
</script>
</body>
</html>
hydrateRoot
修改代码 ReactDOM.createRoot(rootElement).render(); 到 ReactDOM.hydrateRoot(rootElement, ); ,代码只需要修改这一句,从截图中可以看到,D 节点没有被替换。
通过这个例子,可以看到 createRoot 和 hydrateRoot 的区别是在于是否替换节点,React 在渲染流程做了某些从而最终达到这种效果,我们看一下 React 的代码是怎么实现的。
在 React 中 Fiber 树的遍历通过 beginWork 和 completeWork 进行的,首先看 beginWork,beginWork 当 React 遇到 HostComponent HTML 节点时,进入 updateHostComponent,updateDateHOstComponent 会调用
tryHydrate 是核心代码,fiber 设置了 stateNode,stateNode 是 fiber 对应的 DOM Node,至此,beginWork 的逻辑结束了。
completeWork
现在我们来看,completeWork 方法中如何处理 hydrateRoot, 首先调用 popHydrationState,然后调用 prepareToHydrateHostInstance,最后调用 markUpdate,在 commit 阶段更新 DOM。
通过 updateFiber 更新节点数据。
总结
hydrateRoot 整体流程是:beginWork 创建节点,completeWork 完成并更新。这个设计还是很巧妙的,将显示和交互分离。