from start

先觉知识

fiber 是什么?

源码位置

从结构来看就是一个构造函数,实例化后生成一个拥有众多属性的Fiber对象节点,每个 Fiber 对应 React element,通过下面三个属性连接形成树,即虚拟DOM树(Fiber 与 VNode 同理,作为树的每个节点)

1
2
3
4
5
6
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

另外

  • 每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)和对应的DOM节点等信息

  • 每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.type = null; // FunctionComponent函数本身,ClassComponent,指class,HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber对应的真实DOM节点

// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;

this.ref = null;

// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}

注意:

React15,Reconciler 采用递归的方式创建虚拟DOM,递归过程是不能中断的。组件树的层级很深,会造成卡顿;

React16 将递归的无法中断的更新重构为异步的可中断更新

双缓存Fiber树

在内存中绘制当前帧内容,绘制完毕后直接用当前帧替换上一帧画面,这种在内存中构建并直接替换的技术叫做双缓存

React使用 “双缓存” 来完成Fiber树的构建与替换——对应着DOM树的创建与更新

双缓存Fiber树:

  • React中最多同时存在两棵Fiber树。屏幕上的称为current Fiber树,内存中的称为workInProgress Fiber树

  • current Fiber 中的 Fiber 节点被称为current fiber,workInProgress Fiber中的 Fiber 节点被称为workInProgress fiber,他们通过 alternate 属性连接

1
2
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
  • React应用的根节点通过 current 指针在不同 Fiber 树的 rootFiber 间切换来实现Fiber树的切换

  • 当 workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

  • 每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成DOM更新

总体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)
|
v
创建Update对象(`updateContainer`)
|
v
从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
v
调度更新(`ensureRootIsScheduled`)
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
v
commit阶段(`commitRoot`)

最初的入口 ReactDOM.render

1、 ReactDOM.render会创建 fiberRootNode 和 rootFiber。其中 fiberRootNode 是整个应用的根节点, rootFiber 是要渲染组件所在组件树的根节点

  • 我们多次调用ReactDOM.render渲染不同的组件树,则会有不同的rootFiber。但整个应用的根节点只有 fiberRootNode

2、 调用 ReactDOM.render 后执行 legacyRenderSubtreeIntoContainer 方法

1
2
3
4
5
6
// legacyRenderSubtreeIntoContainer 
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;

3、legacyCreateRootFromDOMContainer 方法内部会调用 createFiberRoot 方法完成 fiberRootNode 和 rootFiber 的创建以及关联。并初始化updateQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// react/packages/react-reconciler/src/ReactFiberRoot.new.js

export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRootNode
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);

// 连接rootFiber与fiberRootNode
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

// 初始化updateQueue
initializeUpdateQueue(uninitializedFiber);

return root;
}
  • 此时 fiberRootNode.current 指向 rootFiber; rootFiber.stateNode 指向 fiberRootNode;由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)

4、初始化节点结束后,执行 unbatchedUpdates,创建更新节点

1
2
3
4
// react/packages/react-dom/src/client/ReactDOMLegacy.js
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});

5、创建Update来开启一次更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// fiberRootNode.current 组件树的根节点
const current = container.current;

// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);

// update.payload为需要挂载在根节点的组件,入口App组件
update.payload = {element};

// 将生成的update加入updateQueue
enqueueUpdate(current, update);

// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);
}

Update

Update 分类:

1
2
3
4
5
ReactDOM.render —— HostRoot
this.setState —— ClassComponent
this.forceUpdate —— ClassComponent
useState —— FunctionComponent
useReducer —— FunctionComponent

共三种组件 HostRoot | ClassComponent | FunctionComponent 可以触发更新,ClassComponent 与 HostRoot 共用一套Update结构,FunctionComponent 单独使用一种Update结构

Update 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
// react/packages/react-reconciler/src/ReactUpdateQueue.new.js
const update: Update<*> = {
eventTime, // 任务时间,通过performance.now()获取的毫秒数
lane, // 优先级相关字段
suspenseConfig,
tag: UpdateState, // 更新的类型 UpdateState | ReplaceState | ForceUpdate | CaptureUpdate

payload: null, // 更新挂载的数据,ClassComponent,payload为this.setState的第一个传参。
// 对于HostRoot,payload为ReactDOM.render的第一个传参

callback: null, // 更新的回调函数
next: null, // 与其他Update连接形成链表
};

存在多个Update情况

1
2
3
4
5
6
7
8
9
onClick() {
this.setState({
a: 1
})

this.setState({
b: 2
})
}

在一个ClassComponent中触发this.onClick方法,方法内部调用了两次this.setState。这会在该fiber中产生两个Update

Fiber节点最多同时存在两个updateQueue:

  • current fiber保存的updateQueue即current updateQueue

  • workInProgress fiber保存的updateQueue即workInProgress updateQueue

在commit阶段完成页面渲染后,workInProgress Fiber树变为current Fiber树,workInProgress Fiber树内Fiber节点的 updateQueue 就变成 current updateQueue

6、 在 updateContainer 内,最后调用 scheduleUpdateOnFiber

1
2
3
4
5
6
7
8
9
10
11
// react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {

// 找到 rootFiber
const root = markUpdateLaneFromFiberToRoot(fiber, lane);

})

markUpdateLaneFromFiberToRoot 核心代码是通过 fiber 的 return(从上面的介绍可以知道return指向了父级) 属性,层层上找知道 rootFiber

1
2
3
4
5
6
7
8
// 核心代码
let parent = sourceFiber.return;
// while 循环直到找到定成return 不再有值,即 rootFiber
while (parent !== null) {
// 此处简化了其他逻辑
node = parent;
parent = parent.return;
}

7、 现在我们拥有 rootFiber和对应的某个Fiber节点包含一个Update,根据更新的优先级,决定以同步还是异步的方式调度本次更新

1
2
3
4
5
6
7
8
// 决定同步还是异步 SyncLane 用二进制数表示
if (lane === SyncLane) {
// ...
ensureRootIsScheduled()
} else {
// ...
ensureRootIsScheduled()
}

ensureRootIsScheduled 核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
// 调度的回调函数
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
// 调度的回调函数
performConcurrentWorkOnRoot.bind(null, root)
);
}

scheduleCallback 和 scheduleSyncCallback 会调用Scheduler提供的调度方法根据优先级调度回调函数执行,从执行 performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 开始,便进入 render 阶段

render

render阶段开始于 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 方法的调用,两个方法中分别调用 renderRootSync 和 renderRootConcurrent,而这两者又分别调用 workLoopSync 和 workLoopConcurrent

workLoopSync 和 workLoopConcurrent 核心区别是 shouldYield

  • shouldYield 如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历,实现了异步可中断
1
2
3
4
5
6
7
8
9
10
11
12
13
// performSyncWorkOnRoot 会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

// performConcurrentWorkOnRoot 会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
  • workInProgress代表当前已创建的workInProgress fiber

  • performUnitOfWork创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树

7、 在 workLoopSync 和 workLoopConcurrent 的 while 循环体内容,performUnitOfWork 不断被调用执行

  • 从 rootFiber 开始向下深度优先遍历。为每个Fiber节点调用beginWork方法。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

  • 当遍历到叶子节点(即没有子组件的组件)时就会进入归阶段,并调用 completeWork

  • 当某个Fiber节点执行完 completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段

  • 如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段

1
2
3
4
5
6
7
8
9
function performUnitOfWork(unitOfWork: Fiber): void {
let next;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
return (
<div>
1
<span>2</span>
</div>
)
}
// 遍历过程,单一文本子节点的Fiber,React会特殊处理,所以没有2
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "1" Fiber beginWork
5. "1" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

beginWork 和 completeWork

8、 beginWork 主要是传入当前Fiber节点,创建子Fiber节点

1
2
3
4
5
6
7
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate

  • workInProgress:当前组件对应的Fiber节点

beginWork 工作分两部分

update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child

mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {

// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}

// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...
case LazyComponent:
// ...
case FunctionComponent:
// ...
case ClassComponent:
// ...
case HostRoot:
// ...
case HostComponent:
// ...
case HostText:
// ...
}
}

常见组件 FunctionComponent/ClassComponent/HostComponent 最终会进入reconcileChildren方法

  • 对于mount的,会创建新的子Fiber节点

  • 对于update的,会将当前组件与该组件在上次更新时对应的Fiber节点比较(Diff算法),将比较的结果生成新Fiber节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     export function reconcileChildren(
    current: Fiber | null,
    workInProgress: Fiber,
    nextChildren: any,
    renderLanes: Lanes,
    ) {
    if (current === null) {
    // mount 组件
    workInProgress.child = mountChildFibers(
    workInProgress,
    null,
    nextChildren,
    renderLanes,
    );
    } else {
    // undate 组件
    workInProgress.child = reconcileChildFibers(
    workInProgress,
    current.child,
    nextChildren,
    renderLanes,
    );
    }
    }
  • 不论哪个逻辑,最终会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress传参

mountChildFibers 和 reconcileChildFibers 实际是同一个方法,只不过用 reconcileChildFibers 会有flag,标记增删改查

1
2
3
4
5
// react/packages/react-reconciler/src/ReactFiberFlags.js
export const Placement = /* */ 0b0000000000000000010;
export const Update = /* */ 0b0000000000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000000000110;
export const Deletion = /* */ 0b0000000000000001000;

9、 在归的阶段 fiber completeWork 开始执行

  • 重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {
// update的情况
// ...
} else {
// mount的情况
// ...
}
return null;
}

update时

Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点

  • onClick、onChange等回调函数的注册

  • 处理style prop

  • 处理DANGEROUSLY_SET_INNER_HTML prop

  • 处理children prop

1
2
3
4
5
6
7
8
9
10
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会commit阶段被渲染在页面上

1
workInProgress.updateQueue = (updatePayload: any);

其中updatePayload为数组形式,他的奇数索引的值为变化的prop key,偶数索引的值为变化的prop value

mount时

  • 为Fiber节点生成对应的DOM节点

  • 将子孙DOM节点插入刚生成的DOM节点中

  • 与update逻辑中的 updateHostComponent 类似的处理 props 的过程

mount 时只会在 rootFiber 存在Placement时。那么commit阶段是如何通过一次插入DOM操作

原因就在于completeWork中的appendAllChildren方法

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树

至此 render 结束,但是还流程还没结束,待续。。。

返回
顶部