React 渲染过程,即
ReactDOM.render
执行过程分为两个大的阶段:render
阶段以及commit
阶段。React.hydrate
渲染过程和ReactDOM.render
差不多,两者之间最大的区别就是,ReactDOM.hydrate
在render
阶段,会尝试复用(hydrate)浏览器现有的 dom 节点,并相互关联 dom 实例和 fiber,以及找出 dom 属性和 fiber 属性之间的差异。
Demo
这里,我们在 index.html
中直接返回一段 html,以模拟服务端渲染生成的 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Mini React</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root"><div id="root"><div id="container"><h1 id="A">1<div id="A2">A2</div></h1><p id="B"><span id="B1">B1</span></p><span id="C">C</span></div></div></div>
</body>
</html>
注意,root
里面的内容不能换行,不然客户端hydrate
的时候会提示服务端和客户端的模版不一致。
新建 index.jsx:
import React from "react";
import ReactDOM from "react-dom";
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 1,
};
}
render() {
const {
count } = this.state;
return (
<div id="container">
<div id="A">
{
count} <div id="A2">A2</div>
</div>
<p id="B">
<span id="B1">B1</span>
</p>
</div>
);
}
}
ReactDOM.hydrate(<Home />, document.getElementById("root"));
对比服务端和客户端的内容可知,服务端h1#A
和客户端的div#A
不同,同时服务端比客户端多了一个span#C
在客户端开始执行之前,即 ReactDOM.hydrate
开始执行前,由于服务端已经返回了 html 内容,浏览器会立马显示内容。对应的真实 DOM 树如下:
注意,这不是 fiber 树!!
ReactDOM.render
先来回顾一下 React 渲染更新过程,分为两大阶段,五小阶段:
- render 阶段
- beginWork
- completeUnitOfWork
- commit 阶段。
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
React 在 render 阶段会根据新的 element tree 构建 workInProgress 树,收集具有副作用的 fiber 节点,构建副作用链表。
特别是,当我们调用ReactDOM.render
函数在客户端进行第一次渲染时,render
阶段的completeUnitOfWork
函数针对HostComponent
以及HostText
类型的 fiber 执行以下 dom 相关的操作:
-
- 调用
document.createElement
为HostComponent
类型的 fiber 节点创建真实的 DOM 实例。或者调用document.createTextNode
为HostText
类型的 fiber 节点创建真实的 DOM 实例
- 调用
-
- 将 fiber 节点关联到真实 dom 的
__reactFiber$rsdw3t27flk
(后面是随机数)属性上。
- 将 fiber 节点关联到真实 dom 的
-
- 将 fiber 节点的
pendingProps
属性关联到真实 dom 的__reactProps$rsdw3t27flk
(后面是随机数)属性上
- 将 fiber 节点的
-
- 将真实的 dom 实例关联到
fiber.stateNode
属性上:fiber.stateNode = dom
。
- 将真实的 dom 实例关联到
-
- 遍历
pendingProps
,给真实的dom
设置属性,比如设置 id、textContent 等
- 遍历
React 渲染更新完成后,React 会为每个真实的 dom 实例挂载两个私有的属性:__reactFiber$
和__reactProps$
,以div#container
为例:
ReactDOM.hydrate
hydrate
中文意思是水合物
,这样理解有点抽象。根据源码,我更乐意将hydrate
的过程描述为:React 在 render 阶段,构造 workInProgress 树时,同时按相同的顺序遍历真实的 DOM 树,判断当前的 workInProgress fiber 节点和同一位置的 dom 实例是否满足hydrate
的条件,如果满足,则直接复用当前位置的 DOM 实例,并相互关联 workInProgress fiber 节点和真实的 dom 实例,比如:
fiber.stateNode = dom;
dom.__reactProps$ = fiber.pendingProps;
dom.__reactFiber$ = fiber;
如果 fiber 和 dom 满足hydrate
的条件,则还需要找出dom.attributes
和fiber.pendingProps
之间的属性差异。
遍历真实 DOM 树的顺序和构建 workInProgress 树的顺序是一致的。都是深度优先遍历,先遍历当前节点的子节点,子节点都遍历完了以后,再遍历当前节点的兄弟节点。因为只有按相同的顺序,fiber 树同一位置的 fiber 节点和 dom 树同一位置的 dom 节点才能保持一致
只有类型为HostComponent
或者HostText
类型的 fiber 节点才能hydrate
。这一点也很好理解,React 在 commit 阶段,也就只有这两个类型的 fiber 节点才需要执行 dom 操作。
fiber 节点和 dom 实例是否满足hydrate
的条件:
-
对于类型为
HostComponent
的 fiber 节点,如果当前位置对应的 DOM 实例nodeType
为ELEMENT_NODE
,并且fiber.type === dom.nodeName
,那么当前的 fiber 可以混合(hydrate) -
对于类型为
HostText
的 fiber 节点,如果当前位置对应的 DOM 实例nodeType
为TEXT_NODE
,同时fiber.pendingProps
不为空,那么当前的 fiber 可以混合(hydrate)
hydrate
的终极目标就是,在构造 workInProgress 树的过程中,尽可能的复用当前浏览器已经存在的 DOM 实例以及 DOM 上的属性,这样就无需再为 fiber 节点创建 DOM 实例,同时对比现有的 DOM 的attribute
以及 fiber 的pendingProps
,找出差异的属性。