本文是一篇入门级源码分析类文章,对于源码了解较深者也可以阅读本文多多指点笔者一二。
(本文不涉及优先级,事件系统,requestIdleCallback的polyfill,hooks等原理的解释)
好文推荐:
从一段简单的代码开始分析
import React from 'react';
import ReactDOM from 'react-dom';
const vdomTree = <div>
<article>
<span>1</span>
</article>
<p>
<strong>s</strong>
</p>
<a >a</a>
</div>;
ReactDOM.render(
vdomTree,
document.getElementById('root')
);
为什么我们要引入React?
这个问题很简单因为以上jsx代码通过@babel/preset-react这样的工具就会自动生成如下代码,你也可以自己在线试一试:
const vdomTree = /*#__PURE__*/
React.createElement("div", null,
/*#__PURE__*/React.createElement("article", null,
/*#__PURE__*/React.createElement("span", null, "1")
),
/*#__PURE__*/React.createElement("p", null,
/*#__PURE__*/React.createElement("strong", null, "s")
),
/*#__PURE__*/React.createElement("a", null, "a")
);
ReactDOM.render(
vdomTree,
document.getElementById('root')
);
我们来打印一下这个vdomTree:
vdomtree结构图
我们用一张图来表示:
每个节点有几个比较重要的属性我们来看一下:
{
?typeof: '通过createElement这个方法创建的都是reactElement类型',
type: '第一个参数',
props:{
children:'当子节点有多个时为一个数组,当只有一个时就是reactElement'
}
}
react是怎么定义fiber结构的?
很多解析源码的文章都说fiber是一个链表结构,其实fiber是用单链表组成的树结构,在源码中这个位置有介绍:
所以基于此处我们应该学习一个数据结构的小知识
如何将一颗树转换成二叉树
这是我在大学的数据结构课本上截的一张图,其实就是将节点的左指针指向第一个孩子节点,右指针指向相邻的兄弟节点即可:
那么这个和fiber结构又有什么关系呢?
fiber结构图
这里与上面的vdomTree图相比较我来画出fiber的结构图,提前给大家看一下:
其实这里我们不难发现这里react里面就是把普通的树转换成了二叉树,有点区别的是每个子节点都有一个指向父节点的return指针,这里用虚线表示
那么react是怎么生成这个fiber结构的呢?我们接着往下看
代码:普通树转化成二叉树
那么把树转换成二叉树的代码该怎么写呢?
vdomTree代码结构如下
let vdomtree = {
type:'div'
children:[{
type:'article',
children:{
type:'span',
children:null
}
},{
type:'p',
children:{
type:'strong',
children:null
}
},{
type:'a',
children:null
}]
}
将以上数据格式转换成fiberTree格式代码如下:
//为什么要初始化两个全局变量?
//为了遍历的方便性,我们先初始化一个头节点,将基础数据结构定义好
let workInProgress = {
tag: 'root',
type: '',
child: null,
sibling:null,
return: null,
vChildren: vdomtree
};
//由于上面的头节点是动态的,当我们完成转换后,需要读取到这课树
//所以还需要一个指针来保存第一个头节点
const root = {
current:workInProgress
};
//开始进行工作
function workLoop(){
while (workInProgress){
workInProgress = performUnitOfWork(workInProgress);
}
console.log(root);
}
//这个函数做了两件事:
//1. beginWork负责创建
//2. completeUnitOfWork负责调整遍历顺序,在源码中此处还会做一些其他事情
function performUnitOfWork(unitOfWork) {
let next = beginWork(unitOfWork)
if(!next){
next = completeUnitOfWork(unitOfWork);
}
return next;
}
//开始生成fiber节点
function beginWork(unitOfWork) {
return updateHostDom(unitOfWork)
}
//当没有子节点时移动到兄弟节点,没有兄弟节点就移动到父节点的兄弟节点
function completeUnitOfWork(unitOfWork) {
do {
let siblingFiber = unitOfWork.sibling;
if (siblingFiber !== null) {
return siblingFiber;
}
unitOfWork = unitOfWork.return;
} while (unitOfWork);
return null;
}
//生成后续节点
function updateHostDom(unitOfWork) {
let vdom = unitOfWork.vChildren;
if(Array.isArray(vdom)){
let firstFiber = null;
let preFiber = null;
for (let i = 0;i < vdom.length;i++){
if(!firstFiber){
firstFiber = {
tag: 'dom',
type: vdom[i].type,
child: null,
sibling:null,
return: unitOfWork,
vChildren: vdom[i].children
}
preFiber = firstFiber;
}else {
preFiber.sibling = {
tag: 'dom',
type: vdom[i].type,
child: null,
sibling:null,
return: unitOfWork,
vChildren: vdom[i].children
}
preFiber = preFiber.sibling;
}
}
return firstFiber;
}else {
return {
tag: 'dom',
type: vdom.type,
child: null,
sibling:null,
return: unitOfWork,
vChildren: vdom.children
}
}
}
workLoop();
看懂以上代码react就是基于此来转换的
生成顺序如下图所示:
从render函数开始分析
打开render函数可以看到它内部就是调用了一下legacyRenderSubtreeIntoContainer函数,这个函数只做了两个事情:
- 生成
fiberRoot - 根据
fiberRoot和传入的vdomtree渲染页面
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
var root = container._reactRootContainer;
var fiberRoot;
if (!root) {
// Initial mount生成fiberRoot和Rootfiber
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate);
fiberRoot = root._internalRoot;
//开始进行更新,由于是初始化所以不需要批处理技术
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
//如果不是第一次更新那么可以复用上次的fiberRoot
fiberRoot = root._internalRoot;
updateContainer(children, fiberRoot, parentComponent, callback);
}
}
头指针创建
createFiberRoot这个方法:
function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
var root = new FiberRootNode(containerInfo, tag, hydrate);
var uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
//在HostRootFiber上初始化一个updateQueue队列
initializeUpdateQueue(uninitializedFiber);
return root;
}
看到这个其实我们就已经知道react在生成fiber架构前会先生成这两个节点,结构如下:
fiberRoot以及HostRootFiber的具体属性
fiberRoot
type BaseFiberRootProperties = {|
// The type of root (legacy, batched, concurrent, etc.)
//
tag: RootTag,
// Any additional information from the host associated with this root.
containerInfo: any,
// Used only by persistent updates.
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
pingCache: WeakMap<Wakeable, Set<mixed>> | Map<Wakeable, Set<mixed>> | null,
// A finished work-in-progress HostRoot that's ready to be committed.
finishedWork: Fiber | null,
// Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
// it's superseded by a new one.
timeoutHandle: TimeoutHandle | NoTimeout,
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
// Used by useMutableSource hook to avoid tearing during hydration.
mutableSourceEagerHydrationData?: Array<
MutableSource<any> | MutableSourceVersion,
> | null,
// Node returned by Scheduler.scheduleCallback. Represents the next rendering
// task that the root will work on.
callbackNode: *,
callbackPriority: LanePriority,
eventTimes: LaneMap<number>,
expirationTimes: LaneMap<number>,
pendingLanes: Lanes,
suspendedLanes: Lanes,
pingedLanes: Lanes,
expiredLanes: Lanes,
mutableReadLanes: Lanes,
finishedLanes: Lanes,
entangledLanes: Lanes,
entanglements: LaneMap<Lanes>,
|};
HostRootFiber
export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
//fiber类型
tag: WorkTag,
// Unique identifier of this child.
//就是你熟悉的key
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// createElement的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 异步组件
type: any,
// The local state associated with this fiber.
// 当前Fiber状态比如dom
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
// 父fiber
return: Fiber | null,
// Singly Linked List Tree Structure.
// 子
child: Fiber | null,
//兄弟
sibling: Fiber | null,
index: number,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
//你熟悉的ref
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// Input is the data coming into process this fiber. Arguments. Props.
// 更新的状态
pendingProps: any, // This type will be more specific once we overload the tag.
//上次的props
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
// Update产生的状态
updateQueue: mixed,
// The state used to create the output
//上次的state
memoizedState: any,
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
// 这是一个比较复杂的节点
mode: TypeOfMode,
// Effect
//标记effect状态
effectTag: SideEffectTag,
subtreeTag: SubtreeTag,
//需要删除的fiber
deletions: Array<Fiber> | null,
// Singly linked list fast path to the next fiber with side-effects.
// 为了查找下个effect fiber
nextEffect: Fiber | null,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
// 子树的第一个fiber
firstEffect: Fiber | null,
// 子树的最后一个fiber
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
// 可以理解成缓存
alternate: Fiber | null,
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};
performSyncWorkOnRoot
此时我们可以发现updateContainer最终会调用它 performSyncWorkOnRoot:
//删减版
function performSyncWorkOnRoot(root) {
//...删掉了expirationTime的计算和fiber优先级的处理
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime$1) {
//初始化一些操作
prepareFreshStack(root, expirationTime);
} // If we have a work-in-progress fiber, it means there's still work to do
// in this root.
//开始调度
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
}
我们可以发现这个函数做了两件事:
- 初始化
- 调度
prepareFreshStack初始化
这个函数很简单只是将fiberRoot赋值给workInProgressRoot,将rootFiber赋值给workInProgress(可以这么理解,内部做了缓存处理)
调度函数workLoopSync
这个函数就是不断使用performUnitOfWork处理workInProgress,下面我们来看下我截取的部分核心逻辑:
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate;
var next = beginWork(current, unitOfWork);
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
return next;
}
function completeUnitOfWork(unitOfWork) {
workInProgress = unitOfWork;
do {
//...删除了收集effect的代码
var siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} // Otherwise, return to the parent
workInProgress = returnFiber;
} while (workInProgress !== null); // We've reached the root.
return null;
}
以上代码其实和刚刚那个思考题是一样的逻辑来生成fiber结构,此时结构图如下所示:
react源码比刚刚笔者自己写的代码多了3个主要逻辑:
- 依赖收集
- diff比对(更新阶段)
- 时间分片机制(更新阶段)
依赖收集(副作用收集)
此处我们要先搞明白一件事,到底收集的是什么?(收集需要更新的fiber节点)
其实我们在初始化生成fiber结构时,我们会给每个节点都标记成新增节点,此处我们会判断节点是否有firstEffect,如果有那么这便是子节点的副作用,即子节点,并且把它挂载到副节点上,继续判断自身是否存在effectTag标记,如果有那么自身也是一个副作用节点,继续把自己挂载到父节点,最终形成一个链表结构。
(上面一段话就是react依赖收集思路,不过react会判断优先级来插入到链表中不同的位置)
依赖收集的顺序
依赖收集是发生在fiber树的构建过程的,在completeUnitOfWork函数中我注释掉的那部分,所以顺序应该是这样的:
diff比对
这是一个可以单独拿出来说一下的,有时间再写,不过这里值得一读的是官方提供的文档,这里可以说是写的很详细了,具体实现在源码ReactChildfiber.js这个文件中的ChildReconciler方法中:
// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
// live outside of this function.
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild(returnFiber: Fiber, childToDelete: Fiber)
function deleteRemainingChildren
function mapRemainingChildren
function useFiber
function placeChild
function placeSingleChild
function updateTextNode
function updateElement
function updatePortal
function updateFragment
function createChild
function updateSlot
function updateFromMap
/**
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey
function reconcileChildrenArray
function reconcileChildrenIterator
function reconcileSingleTextNode
function reconcileSingleElement
function reconcileSinglePortal
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers
return reconcileChildFibers;
}
在这个函数上面有一段注释,大致意思就是这是一个包裹着很多不同分支的reconcile代码,这样react官方会做很多针对性的优化。
时间分片
这其实用一个新的api就可以实现,不过react实现了自己的polyfill版本,可以下次再说react是怎么实现的,这里可以简单看一下这个api requestIdleCallback其实很简单:
假如你的页面有这么一个动画:
div{
width: 100px;
height: 100px;
background-color: red;
transition: height 20s ease;
}
div:hover {
height: 3000px;
}
当用户打开页面鼠标移动到div上时,这时页面有一个work计算在页面打开后2s时执行,耗时5秒:
function work(allTime){
var s = performance.now();
var currentCompletetTime = performance.now() - s;
while(currentCompletetTime < allTime){
currentCompletetTime = performance.now() - s;
}
console.log('complete--------------');
}
setTimeout(work,2000,5000);
此时我们会发现动画会在2秒时卡住,这个时候我们改造代码:
var st = null;
function work(allTime){
clearTimeout(st);
var unit = 1000;
var s = performance.now();
var currentCompletetTime = performance.now() - s;
while(currentCompletetTime < unit && currentCompletetTime < allTime){
currentCompletetTime = performance.now() - s;
}
allTime = allTime - currentCompletetTime;
console.log(allTime)
if(allTime > 0){
st = setTimeout(()=>{
work(allTime)
},100);
}else{
console.log('complete--------------');
}
}
setTimeout(work,2000,5000);
这时我们会发现随着我们手动设置unit的值越小,动画越流畅,那么有没有什么办法让浏览器自己来决定什么怎么协调呢?答案就是requestIdleCallback它可以让浏览器在每一帧的空闲时间执行代码,于是我们再次改造代码:
function workUnit(t){
var allTime = t;
window.requestIdleCallback((IdleDeadline)=>{
var s = performance.now();
var currentCompletetTime = performance.now() - s;
while(currentCompletetTime < allTime && IdleDeadline.timeRemaining() > 0){
currentCompletetTime = performance.now() - s;
}
allTime = allTime - currentCompletetTime;
console.log('restTime:' + allTime,'unit:' + currentCompletetTime);
if(allTime>0){
workUnit(allTime);
}else{
console.log('complete--------------');
}
})
}
setTimeout(workUnit,2000,5000);
这里大概列举了一个小案例,react就是在workloop时用这样的方式去中断fiber的创建,diff等过程。
以上就是react的第一个阶段,它是可以中断的,该阶段结束就会进入commit阶段,commit阶段是不可以中断的,commit阶段会将dom渲染到页面上
事件系统截图
这里截取了源码中的一些注释,有兴趣的话可以自己翻翻源码看看具体细节:
/**
* Summary of `DOMEventPluginSystem` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginRegistry` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginRegistry` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|PluginRegistry| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
-----------我是分割线:下面的内容大概讲了一下总体的原理,有兴趣的话可以阅读一下--------------
先上一张总体架构图
简化了很多流程,这里只取主要流程
主流程解说
- 从render函数起步,render函数第一个参数将接受一个vdom-tree。
- 通过scheduleRoot开始调度,shceduleRoot会使用到workInProgressRoot,nextUnitOfWork,currentRoot以及workInProgressRoot.alternate这几个关键元素
- 我们会通过requestIdleCallBack去执行wookloop(react中自己实现了该函数的polyfill,这里不会讲述)
- 通过! deadline.timeRemaining() < 1 && nextUnitOfWork,判断时间片是否还有剩余并且nextUnitOfWork任务还有剩余去循环执行performUnitOfWork(nextUnitOfWork),该函数的作用是生成fiber-tree以及收集更新时的依赖。
- 以上调度过程就是可以被打断的,当没有nextUnitOfWork时,将调用commitRoot更新页面
wookloop的流程详解
基于我发的整体架构图
- workLoop函数会首先判断是否存在任务,并且该时间片是否使用完(在此我们可以先不考虑时间片使用完成的情况),当任务存在,会调用performUnitOfWork,该函数会立即调用beginWork对当前fiberNode即nextUnitOfWork的子节点进行调和,生成新的fiber-tree。(这里只是部分fiber-tree形成)
- 当该节点调和完成,react会将currentFiber指向他的child,并将该节点return出去赋值给nextUnitOfWork再次调用wookLoop直到child为空。
- 此时从该节点开始收集依赖,(注意fiber-tree并没有完全形成),调用completeUnitOfWork(currentFiber)开始收集依赖,当该fiber-node依赖收集完成,react将该节点移向他的sibling节点,并将该节点赋值给nextUnitOfWork,再次调用performUnitOfWork,此时会生成该节点的fiber-tree,并且由于sibling节点没有child,将再次调用completeUnitOfWork收集该节点依赖。
- 在第3步中,如果sibling节点没有,react将会移动当前节点到到其父节点,再次通过completeUnitOfWork收集父节点依赖。
- 重复3,4过程直到当前节点为空,此时fiber-tree形成完成,依赖收集完成。
- 记住一点以上过程是通过requestIdleCallBack执行的,那么以上过程便可以被打断。
如何调和?
先上图片
流程解说
- 首先根据是否存在oldFiber来判断是否是第一次渲染,如果是接下来的判断都属于新增节点,如果不是那么会更具新老节点的diff比对形成新的fiber-tree,
- 比对时我们会顺序遍历新老节点,(源码中会根据key形成map数据结构来比对节点)这里的比对方式和官网给出的说法是一致的,这里主要说的使一些性能上的优化,当react判断是第一次更新(不是第一次渲染),他会复用oldFiber,如果是第二次或者更多次更新,react会复用用缓存在alternate节点上的老节点,从而生成新的fiber-tree
- 调和的目的就是形成新的fiber-tree
收集依赖?
再来看一张图片
- 更具以上分析当开始调用completeUnitOfWork收集依赖时,该节点是最后一个child元素,从该元素开始,收集该元素的effect及其child的effect(最后一个元素无child)(整张图就是这个意思)
- 按照本身--->兄弟节点---->父节点的顺序向上遍历,直到rootFiber节点所有依赖收集完毕。(根据整个架构图推出)
提交fiberRoot
上图:
- 当收集完依赖,我们开始执行commitRoot,首先我们删除需要删除的节点,接着会从fistEffect开始遍历更新页面dom节点
- 调用commitWork(currentFiber)更新此fiber的dom节点,首先我们会寻找到该fiber节点的第一个父容器dom节点,在更具该节点effectTag来更新、删除或者除添加节点。
- 将effect指向下一个重读2过程,直到所有effect遍历完成。