一、前言
本文将会通过一个简单的例子,结合React源码(v 16.4.2)来说明 React 是如何工作的,并且帮助读者理解 ReactElement、Fiber 之间的关系,以及 Fiber 在各个流程的作用。看完这篇文章有助于帮助你更加容易地读懂 React 源码。初期计划有以下几篇文章:
首次渲染
事件绑定
更新流程
调度机制
二、核心类型解析
在正式进入流程讲解之前,先了解一下 React 源码内部的核心类型,有助于帮助我们更好地了解整个流程。为了让大家更加容易理解,后续的描述只抽取核心部分,把 ref、context、异步、调度、异常处理 之类的简化掉了。
1. ReactElement
我们写 React 组件的时候,通常会使用JSX来描述组件。
这种写法经过babel转换后,会变成以 React.createElement(type, props, children)形式。而我们的例子中,type会是两种类型:function、string,实际上就是App的constructor方法,以及其他HTML标签。
而这个方法,最终是会返回一个 ReactElement ,他是一个普通的 Object ,不是通过某个 class 实例化二来的,大概看看即可,核心成员如下:
key type desc
$$typeof Symbol|Number 对象类型标识,用于判断当前Object是否一个某种类型的ReactElement
type Function|String|Symbol|Number|Object 如果当前ReactElement是是一个ReactComponent,那这里将是它对应的Constructor;而普通HTML标签,一般都是String
props Object ReactElement上的所有属性,包含children这个特殊属性
2. ReactRoot
当前放在ReactDom.js内部,可以理解为React渲染的入口。我们调用ReactDom.render之后,核心就是创建一个 ReactRoot ,然后调用 ReactRoot 实例的render方法,进入渲染流程的。
key type desc
render Function 渲染入口方法
_internalRoot FiberRoot 根据当前DomContainer创建的一个FiberTree的根
3. FiberRoot
FiberRoot 是一个 Object ,是后续初始化、更新的核心根对象。核心成员如下:
key type desc
current (HostRoot)FiberNode 指向当前已经完成的Fiber Tree 的Root
containerInfo DomContainer 根据当前DomContainer创建的一个FiberTree的根
finishedWork (HostRoot)FiberNode|null 指向当前已经完成准备工作的Fiber Tree Root
current、finishedWork,都是一个(HostRoot)FiberNode,到底是为什么呢?先卖个关子,后面将会讲解。
4. FiberNode
在 React 16之后,Fiber Reconciler 就作为 React 的默认调度器,核心数据结构就是由FiberNode组成的 Node Tree 。先参观下他的核心成员:
key type desc
实例相关 --- ---
tag Number FiberNode的类型,可以在packages/shared/ReactTypeOfWork.js中找到。当前文章 demo 可以看到ClassComponent、HostRoot、HostComponent、HostText这几种
type Function|String|Symbol|Number|Object 和ReactElement表现一致
stateNode FiberRoot|DomElement|ReactComponentInstance FiberNode会通过stateNode绑定一些其他的对象,例如FiberNode对应的Dom、FiberRoot、ReactComponent实例
Fiber遍历流程相关
return FiberNode|null 表示父级 FiberNode
child FiberNode|null 表示第一个子 FiberNode
sibling FiberNode|null 表示紧紧相邻的下一个兄弟 FiberNode
alternate FiberNode|null Fiber调度算法采取了双缓冲池算法,FiberRoot底下的所有节点,都会在算法过程中,尝试创建自己的“镜像”,后面将会继续讲解
数据相关
pendingProps Object 表示新的props
memoizedProps Object 表示经过所有流程处理后的新props
memoizedState Object 表示经过所有流程处理后的新state
副作用描述相关
updateQueue UpdateQueue 更新队列,队列内放着即将要发生的变更状态,详细内容后面再讲解
effectTag Number 16进制的数字,可以理解为通过一个字段标识n个动作,如Placement、Update、Deletion、Callback……所以源码中看到很多 &=
firstEffect FiberNode|null 与副作用操作遍历流程相关 当前节点下,第一个需要处理的副作用FiberNode的引用
nextEffect FiberNode|null 表示下一个将要处理的副作用FiberNode的引用
lastEffect FiberNode|null 表示最后一个将要处理的副作用FiberNode的引用
5. Update
在调度算法执行过程中,会将需要进行变更的动作以一个Update数据来表示。同一个队列中的Update,会通过next属性串联起来,实际上也就是一个单链表。
key type desc
tag Number 当前有0~3,分别是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
payload Function|Object 表示这个更新对应的数据内容
callback Function 表示更新后的回调函数,如果这个回调有值,就会在UpdateQueue的副作用链表中挂在当前Update对象
next Update UpdateQueue中的Update之间通过next来串联,表示下一个Update对象
6. UpdateQueue
在 FiberNode 节点中表示当前节点更新、更新的副作用(主要是Callback)的集合,下面的结构省略了CapturedUpdate部分
key type desc
baseState Object 表示更新前的基础状态
firstUpdate Update 第一个 Update 对象引用,总体是一条单链表
lastUpdate Update 最后一个 Update 对象引用
firstEffect Update 第一个包含副作用(Callback)的 Update 对象的引用
lastEffect Update 最后一个包含副作用(Callback)的 Update 对象的引用
三、代码样例
本次流程说明,使用下面的源码进行分析
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
, document.getElementById('root'));
//App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
msg:'init',
};
}
render() {
return (
To get started, edit {this.state.msg} and save to reload.
);
}
}
export default App;
四、渲染调度算法 - 准备阶段
从ReactDom.render方法开始,正式进入渲染的准备阶段。
1. 初始化基本节点
创建 ReactRoot、FiberRoot、(HostRoot)FiberNode,建立他们与 DomContainer 的关系。
2. 初始化(HostRoot)FiberNode的UpdateQueue
通过调用ReactRoot.render,然后进入packages/react-reconciler/src/ReactFiberReconciler.js的updateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate一系列方法调用,为这次初始化创建一个Update,把
这个 ReactElement 作为 Update 的payload.element的值,然后把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。
然后调用scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot,期间主要是提取当前应该进行初始化的 (HostFiber)FiberNode,后续正式进入算法执行阶段。
五、渲染调度算法 - 执行阶段
由于本次是初始化,所以需要调用packages/react-reconciler/src/ReactFiberScheduler.js的renderRoot方法,生成一棵完整的FiberNode Tree finishedWork。
1. 生成 (HostRoot)FiberNode 的workInProgress,即current.alternate。
在整个算法过程中,主要做的事情是遍历 FiberNode 节点。算法中有两个角色,一是表示当前节点原始形态的current节点,另一个是表示基于当前节点进行重新计算的workInProgress/alternate节点。两个对象实例是独立的,相互之前通过alternate属性相互引用。对象的很多属性都是先复制再重建的。
第一次创建结果示意图:
这个做法的核心思想是双缓池技术(double buffering pooling technique),因为需要做 diff 的话,起码是要有两棵树进行对比。通过这种方式,可以把树的总体数量限制在2,节点、节点属性都是延迟创建的,最大限度地避免内存使用量因算法过程而不断增长。后面的更新流程的文章里,会了解到这个双缓冲怎么玩。
2. 工作执行循环
示意代码如下:
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
nextRenderExpirationTime,
);
....
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
刚刚创建的 FiberNode 被作为nextUnitOfWork,从此进入工作循环。从上面的代码可以看出,在是一个典型的递归的循环写法。这样写成循环,一来就是和传统的递归改循环写法一样,避免调用栈不断堆叠以及调用栈溢出等问题;二来在结合其他Scheduler代码的辅助变量,可以实现遍历随时终止、随时恢复的效果。
我们继续深入performUnitOfWork函数,可以看到类似的代码框架:
const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
//...
return next;
从这里可以看出,这里对 workInProgress 节点进行一些处理,然后会通过一定的遍历规则返回next,如果next不为空,就返回进入下一个performUnitOfWork,否则就进入completeUnitOfWork。
3. beginWork
每个工作的对象主要是处理workInProgress。这里通过workInProgress.tag区分出当前 FiberNode 的类型,然后进行对应的更新处理。下面介绍我们例子里面可以遇到的两种处理比较复杂的 FiberNode 类型的处理过程,然后再单独讲解里面比较重要的processUpdateQueue以及reconcileChildren过程。
3.1 HostRoot - updateHostRoot
HostRoot,即文中经常讲到的 (HostRoot)FiberNode,表示它是一个 HostRoot 类型的 FiberNode ,代码中通过FiberRoot.tag表示。
前面讲到,在最开始初始化的时候,(HostRoot)FiberNode 在初始化之后,初始化了他的updateQueue,里面放了准备处理的子节点。这里就做两个动作:
处理更新队列,得出新的state - processUpdateQueue方法
创建或者更新 FiberNode 的child,得到下一个工作循环的入参(也是FiberNode) - ChildReconciler方法
通过这两个函数的详细内容属于比较通用的部分,将在后面单独讲解。
3.2 ClassComponent - updateClassComponent
ClassComponent,即我们在写 React 代码的时候自己写的 Component,即例子中的App。
3.2.1 创建ReactComponent实例阶段
对于尚未初始化的节点,这个方法主要是通过FiberNode.type这个 ReactComponent Constructor 来创建 ReactComponent 实例并创建与 FiberNode 的关系。
(ClassComponent)FiberNode 与 ReactComponent 的关系示意图:
初始化后,会进入实例的mount过程,即把 Component render之前的周期方法都调用完。期间,state可能会被以下流程修改:
调用getDerivedStateFromProps
调用componentWillMount -- deprecated
处理因上面的流程产生的Update所调用的processUpdateQueue
3.2.2 完成阶段 - 创建 child FiberNode
在上面初始化Component实例之后,通过调用实例的render获取子 ReactElement,然后创建对应的所有子 FiberNode 。最终将workInProgress.child指向第一个子 FiberNode。
3.4 处理节点的更新队列 - processUpdateQueue 方法
在解释流程之前,先回顾一下updateQueue的数据结构:
从上面的结构可以看出,UpdateQueue 是存放整个 Update 单向链表的容器。里面的 baseState 表示更新前的原始 State,而通过遍历各个 Update 链表后,最终会得到一个新的 baseState。
对于单个 Update 的处理,主要是根据Update.tag来进行区分处理。
ReplaceState:直接返回这里的 payload。如果 payload 是函数,则使用它的返回值作为新的 State。
CaptureUpdate:仅仅是将workInProgress.effectTag设置为清空ShouldCapture标记位,增加DidCapture标记位。
UpdateState:如果payload是普通对象,则把他当做新 State。如果 payload 是函数,则把执行函数得到的返回值作为新 State。如果新 State 不为空,则与原来的 State 进行合并,返回一个新对象。
ForceUpdate:仅仅是设置 hasForceUpdate为 true,返回原始的 State。
整体而言,这个方法要做的事情,就是遍历这个 UpdateQueue ,然后计算出最后的新 State,然后存到workInProgress.memoizedState中。
3.5 处理子FiberNode - reconcileChildren 方法
在 workInProgress 节点自身处理完成之后,会通过props.children或者instance.render方法获取子 ReactElement。子 ReactElement 可能是对象、数组、字符串、迭代器,针对不同的类型进行处理。
下面通过 ClassComponent 及其 数组类型 child的场景来讲解子 FiberNode 的创建、关联流程(reconcileChildrenArray方法):
在页面初始化阶段,由于没有老节点的存在,流程上就略过了位置索引比对、兄弟元素清理等逻辑,所以这个流程相对简单。
遍历之前render方法生成的 ReactElement 数组,一一对应地生成 FiberNode。FiberNode 有returnFiber属性和sibling属性,分别指向其父亲 FiberNode和紧邻的下一个兄弟 FiberNode。这个数据结构和后续的遍历过程相关。
现在,生成的FiberNode Tree 结构如下:
图中的两个(HostComponent)FiberNode就是刚刚生成的子 FiberNode,即源码中的
...
与
。这个方法最后返回的,是第一个子 FiberNode,就通过这种方式创建了(ClassComponent)FiberNode.child与第一个子 FiberNode的关系。
这个时候,再搬出刚刚曾经看过的代码:
const current = workInProgress.alternate;
//...
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
//...
return next;
意味着刚刚返回的 child 会被当做 next 进入下一个工作循环。如此往复,会得到下面这样的 FiberNode Tree :
生成这棵树之后,被返回的是左下角的那个 (HostText)FiberNode。而重新进入beginWork方法后,由于这个 FiberNode 并没有 child ,根据上面的代码逻辑,会进入completeUnitOfWork方法。
注意:虽然说本例子的 FiberNode Tree 最终形态是这样子的,但实际上算法是优先深度遍历,到叶子节点之后再遍历紧邻的兄弟节点。如果兄弟节点有子节点,则会继续扩展下去。
4. completeUnitOfWork
进入这个流程,表明 workInProgress 节点是一个叶子节点,或者它的子节点都已经处理完成了。现在开始要完成这个节点处理的剩余工作。
4.1 创建DomElement,处理子DomElement 绑定关系
completeWork方法中,会根据workInProgress.tag来区分出不同的动作,下面挑选2个比较重要的来进一步分析:
4.1.1 HostText
此前提到过,FiberNode.stateNode可以用于存放 DomElement Instance。在初始化过程中,stateNode 为 null,所以会通过document.createTextNode创建一个 Text DomElement,节点内容就是workInProgress.memoizedProps。最后,通过__reactInternalInstance$[randomKey]属性建立与自己的 FiberNode的联系。
4.1.2 HostComponent
在本例子中,处理完上面的 HostText 之后,调度算法会寻找当前节点的 sibling 节点进行处理,所以进入了HostComponent的处理流程。
由于当前出于初始化流程,所以处理比较简单,只是根据FiberNode.tag(当前值是code)来创建一个 DomElement,即通过document.createElement来创建节点。然后通过__reactInternalInstance$[randomKey]属性建立与自己的 FiberNode的联系;通过__reactEventHandlers$[randomKey]来建立与 props 的联系。
完成 DomElement 自身的创建之后,如果有子节点,则会将子节点 append 到当前节点中。现在先略过这个步骤。
后续,通过setInitialProperties方法对 DomElement 的属性进行初始化,而
节点的内容、样式、class、事件 Handler等等也是这个时候存放进去的。
现在,整个 FiberNode Tree 如下:
经过多次循环处理,得出以下的 FiberNode Tree:
之后,回到红色箭头指向的 (HostComponent)FiberNode,可以分析一下之前省略掉的子节点处理流程。
在当前 DomElement 创建完毕后,进入appendAllChildren方法把子节点 append 到当前 DomElement 。由上面的流程可以知道,可以通过 workInProgress.child -> workInProgress.child.sibling -> workInProgress.child.sibling.sibling ....找到所有子节点,而每个节点的 stateNode 就是对应的 DomElement,所以通过这种方式的遍历,就可以把所有的 DomElement 挂载到 父 DomElement中。
最终,和 DomElement 相关的 FiberNode 都被处理完,得出下面的FiberNode 全貌:
4.2 将当前节点的 effect 挂在到 returnFiber 的 effect 末尾
在前面讲解基础数据结构的时候描述过,每个 FiberNode 上都有 firstEffect、lastEffect ,指向一个Effect(副作用) FiberNode链表。在处理完当前节点,即将返回父节点的时候,把当前的链条挂接到 returnFiber 上。最终,在(HostRoot)FiberNode.firstEffect 上挂载着一条拥有当前 FiberNode Tree 所有副作用的 FiberNode 链表。
5. 执行阶段结束
经历完之前的所有流程,最终 (HostRoot)FiberNode 也被处理完成,就把 (HostRoot)FiberNode 返回,最终作为finishedWork返回到 performWorkOnRoot,后续进入下一个阶段。
六、渲染调度算法 - 提交阶段
所谓提交阶段,就是实际执行一些周期函数、Dom 操作的阶段。
这里也是一个链表的遍历,而遍历的就是之前阶段生成的 effect 链表。在遍历之前,由于初始化的时候,由于 (HostRoot)FiberNode.effectTag为Callback(初始化回调)),会先将 finishedWork 放到链表尾部。结构如下:
每个部分提交完成之后,都会把遍历节点重置到finishedWork.firstEffect。
1. 提交节点装载( mount )前的操作
当前这个流程处理的只有属于 ReactComponent 的 getSnapshotBeforeUpdate方法。
2. 提交端原生节点( Host )的副作用(插入、修改、删除)
遍历到某个节点后,会根据节点的 effectTag 决定进行什么操作,操作包括插入( Placement )、修改( Update )、删除( Deletion )。
由于当前是首次渲染,所以会进入插入( Placement )流程,其余流程将在后面的《How React Works(三)更新流程》中讲解。
2.1 插入流程( Placement )
要做插入操作,必先找到两个要素:父亲 DomElement ,子 DomElement。
2.1.1 找到相对于当前 FiberNode 最近的父亲 DomElement
通过FiberNode.return不断往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode节点,然后通过(HostComponent)FiberNode.stateNode、(HostRoot)FiberNode.stateNode.containerInfo、(HostPortal)FiberNode.stateNode.containerInfo就可以获取到对应的 DomElement 实例。
2.1.2 找到相对于当前 FiberNode 最近的所有游离子 DomElement
实际上,把目标是查找当前 FiberNode底下所有邻近的 (HostComponent)FiberNode、(HostText)FiberNode,然后通过 stateNode 属性就可以获取到待插入的 子DomElement 。
所谓所有邻近的,可以通过这幅图来理解:
图中红框部分FiberNode.stateNode,就是要被添加到父亲 DomElement的 子 DomElement。
遍历顺序,和之前的生成 FiberNode Tree时顺序大致相同:
a) 访问child节点,直至找到 FiberNode.type 为 HostComponent 或者 HostRoot 的节点,获取到对应的 stateNode ,append 到 父 DomElement中。
b) 寻找兄弟节点,如果有,就访问兄弟节点,返回 a) 。
c) 如果没有兄弟节点,则访问 return 节点,如果 return 不是当前算法入参的根节点,就返回a)。
d) 如果 return 到根节点,则退出。
3. 改变 workInProgress/alternate/finishedWork 的身份
虽然是短短的一行代码,但这个十分重要,所以单独标记:
root.current = finishedWork;
这意味着,在 DomElement 副作用处理完毕之后,意味着之前讲的缓冲树已经完成任务,翻身当主人,成为下次修改过程的current。再来看一个全貌:
4. 提交装载、变更后的生命周期调用操作
在这个流程中,也是遍历 effect 链表,对于每种类型的节点,会做不同的处理。
4.1 ClassComponent
如果当前节点的 effectTag 有 Update 的标志位,则需要执行对应实例的生命周期方法。在初始化阶段,由于当前的 Component 是第一次渲染,所以应该执行componentDidMount,其他情况下应该执行componentDidUpdate。
之前讲到,updateQueue 里面也有 effect 链表。里面存放的就是之前各个 Update 的 callback,通常就来源于setState的第二个参数,或者是ReactDom.render的 callback。在执行完上面的生命周期函数后,就开始遍历这个 effect 链表,把 callback 都执行一次。
4.2 HostRoot
操作和 ClassComponent 处理的第二部分一致。
4.3 HostComponent
这部分主要是处理初次加载的 HostComponent 的获取焦点问题,如果组件有autoFocus这个 props ,就会获取焦点。
七、小结
本文主要讲述了ReactDom.render的内部的工作流程,描述了 React 初次渲染的内在流程:
创建基础对象: ReactRoot、FiberRoot、(HostRoot)FiberNode
创建 HostRoot 的镜像,通过镜像对象来做初始化
初始化过程,通过 ReactElement 引导 FiberNode Tree 的创建
父子 FiberNode 通过child、return连接
兄弟 FiberNode 通过sibling连接
FiberNode Tree 创建过程,深度优先,到底之后创建兄弟节点
一旦到达叶子节点,就开始创建 FiberNode 对应的 实例,例如对应的 DomElement 实例、ReactComponent 实例,并将实例通过FiberNode.stateNode创建关联。
如果当前创建的是 ReactComponent 实例,则会调用调用getDerivedStateFromProps、componentWillMount方法
DomElement 创建之后,如果 FiberNode 子节点中有创建好的 DomElement,就马上 append 到新创建的 DomElement 中
构建完成整个FiberNode Tree 后,对应的 DomElement Tree 也创建好了,后续进入提交过程
在创建 DomElement Tree 的过程中,同时会把当前的副作用不断往上传递,在提交阶段里面,会找到这种标记,并把刚创建完的 DomElement Tree 装载到容器 DomElement中
双缓冲的两棵树 FiberNode Tree 角色互换,原来的 workInProgress 转正
执行对应 ReactComponent 的装载后生命周期方法componentDidMount
其他回调调用、autoFocus 处理
下一篇文章将会描述 React 的事件机制(但据说准备要重构),希望我不会断耕。https://www.cnblogs.com/lcllao/p/9642376.html