上一篇文章我们给出了一个例子,但是只说了 React.createElement
, 留下了 ReactDOM.render
。ReactDOM.render
内含的操作就非常多了,所以必须进行拆解。
我们把 ReactDOM.render
按照功能及调用顺序来划分可以分成三大块:schedule (调度)、reconciliation(协调)、render(渲染)。
简单说一下这三大块都干了什么事情:
- scheduler(调度):调度任务的优先级,高任务优先进入下一阶段,也就是 Reconciliation(协调) 阶段
- reconciliation(协调):找出变化了的组件
- render(渲染):将变化的组件渲染到页面上
因为做的事情非常多,所以 ReactDOM.render
的调用栈特别深,直接看代码一个一个去梳理很容易蒙圈,所以我找了一个 React 生成的界面,使用 Chrome 的 Performance 进行录制,可以看到其调用栈如下图所示:
上图的三个框基本上就代表了三大功能块儿。
我们不按照三大功能块儿的调用顺序来理解,因为这样去学习 react 源码可能会比较困难,所以我打算先搞清楚到底什么是 Fiber
,这是一个贯穿始终的东西。
什么是 Fiber
Fiber
是一种特殊的数据结构(一个特殊的链表,链接着父兄子),也是一种虚拟 DOM 的实现,支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
一个 ReactElement
会对应一个 Fiber
对象。
源码
Fiber
的源码在 packages/react-reconciler/src/ReactFiber.new.js
路径下
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他 Fiber 节点形成 Fiber 树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps; // 待使用的 props
this.memoizedProps = null; // 已使用的 props
this.updateQueue = null; // 状态更新或回调链表
this.memoizedState = null; // 状态
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // fiber 节点包含的副作用标识
this.subtreeFlags = NoFlags; // 子树包含的副作用标识,避免深度遍历
this.deletions = null; // 删除的节点,用于执行 unmount 钩子
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该 fiber 在另一次更新时对应的 fiber
this.alternate = null;
// 省略后面一些代码
}
解释几个比较重要的属性:
- tag 当前节点类型的一个标记,比如:根节点的 tag 为 3;函数组件 tag 为 0。
ReactWorkTags.js
有完整定义 - key 就是 key属性
- stateNode
Fiber
对应的真实 DOM 节点 - elementType 是真实 DOM 元素的标签,比如:h3、div 等等
- return、child、sibling 分别是父、子、兄
fiber
节点 - Effects 相关的属性,记录着是否需要更新或删除的一个标记,
ReactFiberFlags.js
有完整的定义 - lanes 是调度阶段使用的
- alternate 是上一次渲染的时候的
fiber
双缓存
什么是双缓存
当我们用canvas
绘制动画,每一帧绘制前都会调用ctx.clearRect
清除上一帧的画面。
如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
这种在内存中构建并直接替换的技术叫做双缓存。
React 中的双缓存
React
使用“双缓存”来完成Fiber树
的构建与替换——对应着DOM树
的创建与更新。
在 React 中最多会同时存在两棵 Fiber树
。当前屏幕显示的Fiber树
叫current Fiber树
,正在内存中构建的 Fiber树
称为 workInProgress Fiber树
,他们通过alternate
属性连接。
根 Fiber
React 首次加载界面的时候会创建一个 FiberRootNode
和 rootFiber
。
FiberRootNode
是整个容器的根节点,整个容器只有一个。
rootFiber
是组件树的根节点,它也是一个 FiberNode
,可以有多个,不同的组件树其组件树的根节点也不一样。
FiberRootNode
的 current
属性会指向当前页面上已经渲染内容对应的 Fiber树
,即 current Fiber树
。
首次渲染的时候,页面中还没有挂载任何 DOM,所以 FiberRootNode
的 current
属性为空,current Fiber树
就为空。
接下来进入reconciliation 阶段的 render 阶段,根据组件返回的 JSX 在内存中依次创建Fiber节点
并连接在一起构建Fiber树
,被称为workInProgress Fiber树
。
在构建workInProgress Fiber树
时会尝试复用current Fiber树
中已有的Fiber节点
内的属性,在首屏渲染时只有rootFiber
存在对应的current fiber
(即rootFiber.alternate
)。
下面是 FiberRootNode
的源码,路径为 packages/react-reconciler/src/ReactFiberRoot.new.js
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
// 省略部分代码
}
createHostRootFiber
函数会帮助初始化 current
属性。
实例
import React from 'react';
import ReactDOM from 'react-dom';
function Com() {
return (
<div style={{ color: 'red' }}>
<h3>Hello, Eagle!</h3>
</div>
)
}
ReactDOM.render(
<Com />,
document.getElementById('root')
);
下面是 div
对应的 FiberNode
的部分属性的一个展示:
alternate: null
child: FiberNode {
alternate: null
child: null
childLanes: 0
dependencies: null
elementType: "h3"
firstEffect: null
flags: 0
index: 0
key: null
lanes: 1
lastEffect: null
memoizedProps: null
memoizedState: null
mode: 8
nextEffect: null
pendingProps: {children: "Hello, eagle!"}
ref: null
return: FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: null, …}
selfBaseDuration: 0
sibling: null
stateNode: null
tag: 5
type: "h3"
updateQueue: null
}
childLanes: 0
dependencies: null
elementType: "div"
firstEffect: null
flags: 2
index: 0
key: null
lanes: 0
lastEffect: null
memoizedProps: null
memoizedState: null
mode: 8
nextEffect: null
pendingProps: {
children: {$$typeof: Symbol(react.element), type: "h3", key: null, ref: null, props: {…}, …}
style: {color: "red"}
}
ref: null
return: FiberNode {
alternate: FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …}
child: FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: null, …}
childLanes: 0
dependencies: null
elementType: null
firstEffect: null
flags: 0
index: 0
key: null
lanes: 0
lastEffect: null
mode: 8
nextEffect: null
pendingProps: null
ref: null
return: null
sibling: null
stateNode: FiberRootNode {tag: 0, containerInfo: div#root, pendingChildren: null, current: FiberNode, pingCache: null, …}
tag: 3
type: null
updateQueue: {baseState: {…}, firstBaseUpdate: null, lastBaseUpdate: null, shared: {…}, effects: null}
}
sibling: null
stateNode: null
tag: 5
type: "div"
updateQueue: null
通过上面这个对象可以看出一下几点:
- 当前
fiber
的return
属性是他的父节点,也是组件的根节点,也是FiberNodeRoot
的current
属性; - 当前
fiber
的父节点的stateNode
属性对应着FiberNodeRoot
,可以看到他的containerInfo
就是我们的真实 DOM 的 root 节点; - 当前
fiber
的pendingProps
属性包含着当前节点的属性; - 当前
fiber
的child
属性是他的子节点,也就是我们的h3
节点。