react源码解析8.render阶段
视频解说(高效学习):进入学习
往期文章:
1.开篇介绍和面试题
2.react的设计理念
3.react源码架构
4.源码目录构造和调试
5.jsx&外围api
6.legacy和concurrent模式入口函数
7.Fiber架构
8.render阶段
9.diff算法
10.commit阶段
11.生命周期
12.状态更新流程
13.hooks源码
14.手写hooks
15.scheduler&Lane
16.concurrent模式
17.context
18事件零碎
19.手写迷你版react
20.总结&第一章的面试题解答
render阶段的入口
render阶段的次要工作是构建Fiber树和生成effectList,在第5章中咱们晓得了react入口的两种模式会进入performSyncWorkOnRoot或者performConcurrentWorkOnRoot,而这两个办法别离会调用workLoopSync或者workLoopConcurrent
//ReactFiberWorkLoop.old.js
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这两函数的区别是判断条件是否存在shouldYield的执行,如果浏览器没有足够的工夫,那么会终止while循环,也不会执行前面的performUnitOfWork函数,天然也不会执行前面的render阶段和commit阶段,这部分属于scheduler的知识点,咱们在第15章解说。
- workInProgress:新创建的workInProgress fiber
-
performUnitOfWork:workInProgress fiber和会和曾经创立的Fiber连接起来造成Fiber树。这个过程相似深度优先遍历,咱们暂且称它们为‘捕捉阶段’和‘冒泡阶段’。伪代码执行的过程大略如下
function performUnitOfWork(fiber) { if (fiber.child) { performUnitOfWork(fiber.child);//beginWork } if (fiber.sibling) { performUnitOfWork(fiber.sibling);//completeWork } }
render阶段整体执行流程
用demo_0看视频调试
- 捕捉阶段
从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入以后Fiber节点,而后创立或复用它的子Fiber节点,并赋值给workInProgress.child。 - 冒泡阶段
在捕捉阶段遍历到子节点之后,会执行completeWork办法,执行实现之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全副兄弟节点执行完之后,会向上‘冒泡’到父节点执行completeWork,直到rootFiber。 -
示例,demo_0调试
function App() { return ( <> <h1> <p>count</p> xiaochen </h1> </> ) } ReactDOM.render(<App />, document.getElementById("root"));
当执行完深度优先遍历之后造成的Fiber树:
图中的数字是遍历过程中的程序,能够看到,遍历的过程中会从利用的根节点rootFiber开始,顺次执行beginWork和completeWork,最初造成一颗Fiber树,每个节点以child和return相连。
留神:当遍历到只有一个子文本节点的Fiber时,该Fiber节点的子节点不会执行beginWork和completeWork,如图中的‘chen’文本节点。这是react的一种优化伎俩
beginWork
beginWork次要的工作是创立或复用子fiber节点
function beginWork(
current: Fiber | null,//以后存在于dom树中对应的Fiber树
workInProgress: Fiber,//正在构建的Fiber树
renderLanes: Lanes,//第12章在讲
): Fiber | null {
// 1.update时满足条件即可复用current fiber进入bailoutOnAlreadyFinishedWork函数
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// ...
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
//2.依据tag来创立不同的fiber 最初进入reconcileChildren函数
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...
case LazyComponent:
// ...
case FunctionComponent:
// ...
case ClassComponent:
// ...
case HostRoot:
// ...
case HostComponent:
// ...
case HostText:
// ...
}
}
从代码中能够看到参数中有current Fiber,也就是以后实在dom对应的Fiber树,在之前介绍Fiber双缓存机制中,咱们晓得在首次渲染时除了rootFiber外,current 等于 null,因为首次渲染dom还没构建进去,在update时current不等于 null,因为update时dom树曾经存在了,所以beginWork函数中用current === null来判断是mount还是update进入不同的逻辑
- mount:依据fiber.tag进入不同fiber的创立函数,最初都会调用到reconcileChildren创立子Fiber
-
update:在构建workInProgress的时候,当满足条件时,会复用current Fiber来进行优化,也就是进入bailoutOnAlreadyFinishedWork的逻辑,能复用didReceiveUpdate变量是false,复用的条件是
- oldProps === newProps && workInProgress.type === current.type 属性和fiber的type不变
- !includesSomeLane(renderLanes, updateLanes) 更新的优先级是否足够,第15章解说
reconcileChildren/mountChildFibers
创立子fiber的过程会进入reconcileChildren,该函数的作用是为workInProgress fiber节点生成它的child fiber即 workInProgress.child。而后持续深度优先遍历它的子节点执行雷同的操作。
//ReactFiberBeginWork.old.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
//mount时
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
//update
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
reconcileChildren会辨别mount和update两种状况,进入reconcileChildFibers或mountChildFibers,reconcileChildFibers和mountChildFibers最终其实就是ChildReconciler传递不同的参数返回的函数,这个参数用来示意是否追踪副作用,在ChildReconciler中用shouldTrackSideEffects来判断是否为对应的节点打上effectTag,例如如果一个节点须要进行插入操作,须要满足两个条件:
- fiber.stateNode!==null 即fiber存在实在dom,实在dom保留在stateNode上
-
(fiber.effectTag & Placement) !== 0 fiber存在Placement的effectTag
var reconcileChildFibers = ChildReconciler(true); var mountChildFibers = ChildReconciler(false);
```js
function ChildReconciler(shouldTrackSideEffects) {
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {//是否追踪副作用
// Noop.
return lastPlacedIndex;
}
var current = newFiber.alternate;
if (current !== null) {
var oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
}
```
在之前心智模型的介绍中,咱们晓得为Fiber打上effectTag之后在commit阶段会被执行对应dom的增删改,而且在reconcileChildren的时候,rootFiber是存在alternate的,即rootFiber存在对应的current Fiber,所以rootFiber会走reconcileChildFibers的逻辑,所以shouldTrackSideEffects等于true会追踪副作用,最初为rootFiber打上Placement的effectTag,而后将dom一次性插入,进步性能。
export const NoFlags = /* */ 0b0000000000000000000;
// 插入dom
export const Placement = /* */ 0b00000000000010;
在源码的ReactFiberFlags.js文件中,用二进制位运算来判断是否存在Placement,例如让var a = NoFlags,如果须要在a上减少Placement的effectTag,就只有 effectTag | Placement就能够了
bailoutOnAlreadyFinishedWork
//ReactFiberBeginWork.old.js
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
//...
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
} else {
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
如果进入了bailoutOnAlreadyFinishedWork复用的逻辑,会判断优先级第12章介绍,优先级足够则进入cloneChildFibers否则返回null
completeWork
completeWork次要工作是解决fiber的props、创立dom、创立effectList
//ReactFiberCompleteWork.old.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
//依据workInProgress.tag进入不同逻辑,这里咱们关注HostComponent,HostComponent,其余类型之后在讲
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case HostRoot:
//...
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update时
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
} else {
// mount时
const currentHostContext = getHostContext();
// 创立fiber对应的dom节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将后辈dom节点插入刚创立的dom里
appendAllChildren(instance, workInProgress, false, false);
// dom节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 解决props和updateHostComponent相似
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
return null;
}
从简化版的completeWork中能够看到,这个函数做了一下几件事
- 依据workInProgress.tag进入不同函数,咱们以HostComponent举例
- update时(除了判断current===null外还须要判断workInProgress.stateNode===null),调用updateHostComponent解决props(包含onClick、style、children …),并将解决好的props赋值给updatePayload,最初会保留在workInProgress.updateQueue上
- mount时 调用createInstance创立dom,将后辈dom节点插入刚创立的dom中,调用finalizeInitialChildren解决props(和updateHostComponent解决的逻辑相似)
之前咱们有说到在beginWork的mount时,rootFiber存在对应的current,所以他会执行mountChildFibers打上Placement的effectTag,在冒泡阶段也就是执行completeWork时,咱们将子孙节点通过appendAllChildren挂载到新创建的dom节点上,最初就能够一次性将内存中的节点用dom原生办法反馈到实在dom中。
在beginWork 中咱们晓得有的节点被打上了effectTag的标记,有的没有,而在commit阶段时要遍历所有蕴含effectTag的Fiber来执行对应的增删改,那咱们还须要从Fiber树中找到这些带effectTag的节点嘛,答案是不须要的,这里是以空间换工夫,在执行completeWork的时候遇到了带effectTag的节点,会将这个节点退出一个叫effectList中,所以在commit阶段只有遍历effectList就能够了(rootFiber.firstEffect.nextEffect就能够拜访带effectTag的Fiber了)
effectList的指针操作产生在completeUnitOfWork函数中,例如咱们的利用是这样的
function App() {
const [count, setCount] = useState(0);
return (
<>
<h1
onClick={() => {
setCount(() => count + 1);
}}
>
<p title={count}>{count}</p> xiaochen
</h1>
</>
)
}
那么咱们的操作effectList指针如下(这张图是操作指针过程中的图,此时遍历到了app Fiber节点,当遍历到rootFiber时,h1,p节点会和rootFiber造成环状链表)
rootFiber.firstEffect===h1
rootFiber.firstEffect.next===p
造成环状链表的时候会从触发更新的节点向上合并effectList直到rootFiber,这一过程产生在completeUnitOfWork函数中,整个函数的作用就是向上合并effectList
//ReactFiberWorkLoop.old.js
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
//...
if (
returnFiber !== null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;//父节点的effectList头指针指向completedWork的effectList头指针
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
//父节点的effectList头尾指针指向completedWork的effectList头指针
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
//父节拍板的effectList尾指针指向completedWork的effectList尾指针
returnFiber.lastEffect = completedWork.lastEffect;
}
const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
//completedWork自身追加到returnFiber的effectList结尾
returnFiber.lastEffect.nextEffect = completedWork;
} else {
//returnFiber的effectList头节点指向completedWork
returnFiber.firstEffect = completedWork;
}
//returnFiber的effectList尾节点指向completedWork
returnFiber.lastEffect = completedWork;
}
}
} else {
//...
if (returnFiber !== null) {
returnFiber.firstEffect = returnFiber.lastEffect = null;//重制effectList
returnFiber.flags |= Incomplete;
}
}
} while (completedWork !== null);
//...
}
最初生成的fiber树如下
而后commitRoot(root);进入commit阶段
发表回复