ltaoo's web

React render 过程中发生了什么

调用 ReactDOM.render 后,会将组件渲染为真实 DOM 并插入指定节点,这个过程具体是怎么完成的呢?本文以 16.8.6 版本进行说明。

首先对一些关键名词作定义

Component ,即组件,下面代码中的 Home 就是组件,继承自 React.Component 或者返回 ReactElement 的函数。

1
2
3
4
5
6
7
8
9
10
11
12
class Home extends React.Component {
render() {
return (
<div>{children}</div>
);
}
}
function Home() {
return (
<div>{children}</div>
);
}

ReactElement,所谓的 virtual dom,耳熟能详的 dom diff 就是 diff 的这玩意。我们手写的 jsx 会被编译为创建 ReactElement 的表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ReactDOM.render(<Home />, document.getElementById('root'));
// 会被编译为
ReactDOM.render(
React.createElement(Home),
document.getElementById('root'),
);
// 调用 createElement 的返回值
ReactDOM.render(
{
$$typeof: 'React.Element',
type: Home,
props: null,
},
document.getElementById('root'),
);

OK,下面开始正文。

按照我们的想象,是不是只需要拿到 ReactElement,调用一下 type 就能获取到真实 DOM,通过 appendChild 方法插入到 document.getElementById('root') 节点中就 OK 了呢?比如这样

1
2
3
4
5
6
7
8
9
10
11
const ReactDOM = {
render(element, container) {
const { type } = element;
if (typeof type === 'function') {
const inst = new type();
const result = inst.render();

container.appendChild(result);
}
},
}

思路上大致是正确的,先说第一个问题,inst.render 方法返回的并不是 Node,而是 ReactElement

1
2
3
4
5
6
7
class Home extends React.Component {
render() {
return (
<div>{children}</div>
);
}
}

<div>{children}</div> 前面也提到了,这是 jsx 语法,所以调用 render 方法后返回的数据实际上是

1
2
3
4
5
6
7
{
$$typeof: 'React.Element',
type: 'div',
props: {
children,
},
}

所以应该是把这种 typeof type === ‘string’ReactElement 生成 DOM,然后插入页面。但这代码有点麻烦,我们干脆调用 ReactDOM.render(<p>hello React</p>, document.getElementById(‘root’)) ,那就肯定不需要 new type() 了,因为 type 就只是字符串了。

所以我们也能想到,ReactElement type 类型的不同,必然会有不同的逻辑去处理。

渲染一个 p 标签到页面上

真实源码其实很复杂,复杂在需要理解非常多的概念,但光用伪代码说明并不能帮助我们理解源码,所以还是以源码来进行说明,不过会对源码做一些删减,先把渲染 p 标签这样一个简单的例子整个流程走通了,再逐渐增加复杂的情况。

首先,我们调用了 ReactDOM.render 方法,经过一系列(不是非常重要的)操作后,最终是调用了 ReactRoot.prototype.render 方法

https://github.com/facebook/react/blob/16.8.6/packages/react-dom/src/client/ReactDOM.js#L373

ReactRoot

这里出现了第一个之前不了解的概念,ReactRoot,从名字上看是 React 应用的「根」,既然是调用实例方法,那肯定要先实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
function ReactRoot({
container,
isConcurrent,
hydrate,
}) {
const root = createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}

ReactRoot.prototype.render = function (children, callback) {
const root = this._internalRoot;
updateContainer(children, root, null);
}

关键在于构造函数内,调用了 createContainer 方法,返回了 root,然后挂载 this 上,所以就是表明,reactRoot._internalRoot 也是一个 root

对的,_internalRoot 从名字上,这是一个「内部」的 root,这个 root 其实是 FiberRoot。之后也只会用到 FiberRoot 而不会再出现 ReactRoot

FiberRoot

这么快又出现了第二个概念,

createContainer -> createFiberRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createFiberRoot(containerInfo, isConcurrent, hydrate) {
// 这是 fiber 类型的
const uninitializedFiber = createHostRootFiber(isConcurrent);

const root = {
current: uninitializedFiber,
containerInfo,
finishedWork: null,
expirationTime: NoWork,

// 必须要有
earliestPendingTime: NoWork,
latestPendingTime: NoWork,
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,

nextScheduledRoot: null,
};

uninitializedFiber.stateNode = root;

return root;
}

所谓的 FiberRoot 其实只是对象字面量,但是非常重要,它有 current 字段,保存了 HostRootFiber

又来了一个新东西,这其实就是 Fiber

Fiber 是什么呢?

Fiber 可以理解成「人」,并且还是已经毕业需要工作的人。

把 Fiber 看作「人」会更好理解

fiber 看作人,把 fiberRoot 看作公司。调用 render 进行初始化的过程,就是公司创立的过程。

假设今天是 2019/08/01,「React 科技有限公司」成立了,作为初始成员的你,担任着技术组长的职责。

公司老大找到你,需要你去组建技术部门,虽然技术部门加上你只需要两位员工就可以了,你想了想今天的时间,拍拍胸脯说,保证在 2019/09/01 前完成任务。

这是你的第一个正式任务,你非常看重它,认为需要好好规划下,这是一次招聘工作,需要在 2019/09/01 前完成,你自言自语道,并拿出随身的硬皮笔记本,写下

  1. 招收一位组员

看了看笔记本,暂时就这一条任务,想了想没有什么问题,于是就向老大反馈说到,我准备好了了,老板知道了你开始招聘,也就放心去做其他事了。

「加油!我能行」,你给自己打气,再次拿出硬皮笔记本,在「已开始」那页写下

  1. 招聘一位组员

你在招聘网站上发布招聘要求,非常顺利地招聘到了一名满意的组员。

我来看看要做啥,记性非常差的你拿出了笔记本,笔记本上有

  • 已完成任务
  • 待完成任务
  • 任务执行步骤

这里怎么理解呢,比如有一条招聘组员任务,你会把这个任务分解成在招聘网发布招聘需求,面试应聘者,办理入职手续三个步骤。步骤是从任务分析得到的。

开始执行步骤,第一步是发布招聘需求,然后面试,面试通过后通知他 2019/08/20 来公司办理入职。

入职当天,你把他领到技术部,登记信息reconcileSingleElement,发放工牌、笔记本、电脑等物资,并安置到了你旁边,从此你就有了第一个小弟reconcileChildren

走流程这个步骤非常长,没有这么简单。到 completeUnitOfWork 都还在处理,就是领着人一直在走流程,childbeginWork 就非常难类比了。

招聘是 OK 了,你的第一个任务完美完成,你拿出笔记本,更新了下已完成记录

然后你想了想,不能让他闲着对吧,你对他说道,你的笔记本上有一些任务,你照着做就行,

于是他也开始了工作updateHostComponent,翻开笔记本,

也没什么要做的嘛,

完成之后,他找到你,说 已经完成任务。