Series article directory (synchronized update)
- React source code analysis series - React's render stage (1): Introduction to the basic
- React source code analysis series - React's render stage (2): beginWork
- React source code analysis series - React's render stage (3): completeUnitOfWork
- React source code analysis series - React's render exception handling mechanism
This series of articles discusses the source code of React v17.0.0-alpha
Let's introduce the "delivery" stage of React Render - beginWork, in "React Source Code Analysis Series - React's Render Stage (1): Basic Process Introduction" We know that beginWork The main function of this is to create Loop ( performUnitOfWork ) child Fiber node of the main body (unitOfWork), the process is as follows:
As can be seen from the above figure, there are four working paths of beginWork:
- Create a new child Fiber node when mount (first screen rendering), and return the new node;
- If the multiplexing conditions are not met during update, a new sub-fiber node will be created as in mount, and the corresponding effectTag will be diffed and hung on the sub-fiber node, and the new node will be returned;
- If the multiplexing conditions are met during update, and it is judged that the descendants of its child nodes still need to be processed, the multiplexed child Fiber node will be returned.
- If the multiplexing conditions are met during update, and it is judged that it is not necessary to continue processing the descendants of its child nodes, the value of
null
will be returned directly;
To summarize:
- The first two are the main working paths;
- The third working path - "multiplexing node" actually has a similar implementation in the second working path - reconcileChildFibers(update), or " multiplexing node " of different levels of ;
- The fourth working path - "directly return
null
value" is an optimization strategy called "pruning" in the "deep traversal" process, which can reduce unnecessary rendering and improve performance.
Input parameters of beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
beginWork has 3 parameters, but for now we only focus on the first two:
- current: The node on the current tree corresponding to the main body of this loop (unitOfWork), namely workInProgress.alternate.
- workInProgress : The main body of this loop (unitOfWork), that is, the Fiber node to be processed.
Determine whether to mount or update
It can be seen from the flow chart of beginWork that the first process branch is to judge whether the current is mount (first screen rendering) or update; the basis for the judgment is: whether the input parameter current is null
, this is because when mount (first screen rendering), The current pointer of null
points to 0620e085c47c4d, and there are still many places that need to be processed differently according to this judgment.
main work path
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
When mount (first screen rendering), it will enter different sub-node creation logic according to different workInProgress.tag
(component types). We focus on the most common component types: FunctionComponent (function component) / ClassComponent (class component) / HostComponent (benchmark). HTML tags), and eventually these logic will enter the reconcileChildren method.
reconcileChildren
Let's take a look at reconcileChildren method:
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,
);
}
}
From the function name - reconcileChildren, you can see that this is the core part of the Reconciler module; here we see that there will be different methods depending on mount (first screen rendering) or update - mountChildFibers | reconcileChildFibers
, but no matter which logic you follow, it will eventually generate The new child Fiber node is assigned to workInProgress.child and used as the loop body (unitOfWork) when the next loop (performUnitOfWork) is executed;
Let's take a look at what these two methods are.
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
As can be seen from the above code, the reconcileChildFibers performed during mount and the mountChildFibers method performed during update are actually encapsulated by the ChildReconciler method, and the difference is only in the parameters passed.
ChildReconciler
Let's take a look ChildReconciler :
// shouldTrackSideEffects 表示是否追踪副作用
function ChildReconciler(shouldTrackSideEffects) {
/* 内部函数集合 */
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) { // 如不需要追踪副作用则直接返回
// Noop.
return;
}
/* 在当前节点(returnFiber)上标记删除目标节点 */
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete]; // 加入“待删除子节点”的数组中
returnFiber.flags |= ChildDeletion; // 标记当前节点需要删除子节点
} else {
deletions.push(childToDelete);
}
}
function placeSingleChild(newFiber: Fiber): Fiber {
/* 标记当前节点插入了新的子节点 */
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
// ...还有其它很多内部函数
/* 主流程 */
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
}
return reconcileChildFibers; // 返回主方法,其中已经通过闭包联系上一堆内部方法了
}
From the above code, we can see that ChildReconciler actually encapsulates a bunch of internal functions through closures. Its main process is actually the method reconcileChildFibers , and the call in the reconcileChildren method is also the call to this reconcileChildFibers method; let's interpret the parameters of this method:
- returnFiber: The current Fiber node, ie workInProgress
- currentFirstChild: the first child Fiber node of the current Fiber node corresponding to the current tree, null when mounted
- newChild: child node (ReactElement)
- lanes: priority related
Then we look back at the input parameter of the ChildReconciler method - shouldTrackSideEffects , this parameter literally means "do you need to track side effects", the so-called "side effects" refers to whether you need to do DOM operations, if necessary, it will be in the current The Fiber node is marked with EffectTag, that is, "tracking" side effects; and only when updating, it is necessary to "track side effects", that is, compare the current Fiber node with the ReactElement after updating the component state this time (diff), and then Get the updated Fiber node and the result of diffing on this node - EffectTag .
Child Node (ReactElement)
Here we need to expand and explain how child node (ReactElement) came from:
- For the jsx code in the component, babel will convert it into a code segment called
React.createElement()
during the compilation phase. - If it is a class component, execute its render member method and get the result of
React.createElement()
execution - a ReactElement object. - If it is a function component, execute it directly, and also get a ReactElement object.
- If it is HostComponent, that is, general HTML, it also gets a ReactElement object.
- For the source code of React.createElement see here .
reconcileChildFibers
In the reconcileChildFibers method, the type of newChild will be judged first to enter different logics.
There are mainly these types:
- ReactElement
- Portal
- React.Lazy wrapped element
- array
- plain text (including numbers and strings)
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) { // 根据$$typeof属性来进一步区分类型
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
// 省略
case REACT_LAZY_TYPE:
// 省略
}
/* 处理子节点是一个数组的情况 */
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 省略
}
/* 处理纯文本 */
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 省略
}
$$typeof
From the above code, we can see that in addition to directly using the data type of newChild
to determine which code branch to take, we also use newChild.$$typeof
to determine, this $$typeof
is the type of the current ReactElement, its value is a Symbol value, and it is already define in advance, we can see that in the factory function of ReactElement, $$typeof
has been copied to REACT_ELEMENT_TYPE
.
Why do you need this $$typeof
attribute? This is because it is necessary to prevent XSS attack : When the application allows to store and echo a JSON object, a malicious user can construct a pseudo ReactElement object , as shown in the following example, if React does not distinguish it, it will directly create the pseudo ReactElement object 1620e085c4820f ReactElement objects are rendered onto the DOM tree. Therefore, since React version 0.14, React will add the $$typeof
attribute to each real ReactElement, and only the ReactElement object with this attribute will be rendered by React; and because this attribute is of Symbol type, it cannot be constructed using JSON, so it can be blocked. live this loophole.
/* 恶意的json对象 */
var xssJsonObject = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 恶意脚本 */'
},
},
// ...
};
reconcileSingleElement
Next, we continue to go down with the processing logic of the ReactElement type as an example, and the reconcileSingleElement method will be called.
Try to reuse the corresponding child Fiber nodes on the current tree
In this method, first there will be such a while loop:
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该child节点的所有sibling节点
const existing = useFiber(child, element.props.children); // 复用child节点
existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
return existing;
}
} else {
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该child节点的所有sibling节点
const existing = useFiber(child, element.props); // 复用child节点
existing.ref = coerceRef(returnFiber, child, element); // 处理ref
existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
return existing;
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child); // 在returnFiber标记删除该子节点
}
child = child.sibling; // 指针指向current树中的下一个节点
}
The function of the above code is to find out all the non-reusable child nodes in the Fiber node corresponding to the current tree in the last update, and mark the effectTag that needs to be deleted in the current Fiber node (returnFiber); the judgment standard is roughly:
- If the key attribute of a current Fiber child node is inconsistent with the child.key in this rendering, mark it for deletion
- Under the premise of the same key attribute: if a current Fiber child node and the child in this rendering are both Fragments, or their elementType attributes are the same, then multiplexing is performed.
The reuse process is basically as follows:
deleteRemainingChildren(returnFiber, child.sibling)
, this is because walking to the reconcileSingleElement method means that the current processing node has only one child node, so after finding a reusable child node, you can mark and delete the remaining (sibling) child nodes.const existing = useFiber(child, element.props);
, call the useFiber method to reuse the child Fiber nodes.existing.return = returnFiber;
, establish the parent-child relationship between the child Fiber node (existing) and the current Fiber node (returnFiber) (attributereturn
).
Unable to reuse, create a new Fiber child node
If there is no reusable child node, it will enter the logic of creating a new child node:
if (element.type === REACT_FRAGMENT_TYPE) {
// ...创建Fragment类型的子节点,忽略
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes); // 根据当前子节点的ReactElement来创建新的Fiber节点
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
The following describes in detail how the child node and how creates a new child node .
Reuse child nodes - useFiber
The multiplexing child node calls the useFiber method. Let's review how this method is called: const existing = useFiber(child, element.props);
.
Here child
refers to the current tree sub-Fiber node that can be reused, and element.props is the props value obtained by ReactElement during this update (this value is also called pendingProps
).
Then we look at the useFiber method itself:
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0; // 重置一下:当前子节点必然为第一个子节点
clone.sibling = null; // 重置一下:当前子节点没有sibling
return clone;
}
It can be seen that this method mainly calls the createWorkInProgress method.
createWorkInProgress
Let's take a look at what createWorkInProgress methods do:
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
/*
如果current.alternate为空(这里先不要理解成是workInProgress),
则复用current节点,再根据本次更新的props来new一个FiberNode对象
*/
if (workInProgress === null) {
// createFiber是Fiber节点(FiberNode)的工厂方法
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode, // mode属性表示渲染模式,一个二进制值
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode; // DOM 节点
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 如果current.alternate不为空,则重置workInProgress的pendingProps/type/effectTag等属性
}
// 复制current的子节点、上次更新时的props和state
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// 复制current的指针
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}
The key points to focus on here are:
- If current.alternate is not empty, then current.alternate should be the tree node of the last update. We can notice that in this scenario, no new Fiber node is created, but the current is directly reused .alternate node (just reset some of its properties), which shows the essence of "double cache", not "every time a new Fiber tree is created, the last updated Fiber tree is discarded. ", but "When creating this updated Fiber tree, try to reuse the last updated Fiber tree to ensure that there are at most two Fiber trees at any time"; and the so-called current and workInProgress are actually relative Yes, it just depends on which Fiber tree the current property of the FiberRootNode points to at this time.
- The node attribute on FiberNode represents the rendering mode, which is a binary value, specifically defined in here .
Create a new child node - createFiberFromElement
The method called to create a brand new child node is createFiberFromElement :
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
It can be seen that the createFiberFromElement method mainly executes the method createFiberFromTypeAndProps , and this method mainly parses and determines the tag and type attributes of the new node, and calls the createFiber method to create a new node object.
reconcileChildrenArray
When a node has multiple child nodes (eg: <div><span>2</span> 3 </div>), then newChild is an array, and it will enter the method of reconcileChildrenArray
Recall how this method is called in the reconcileChildFibers method:
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber, // 当前的Fiber节点
currentFirstChild, // current树中对应的子Fiber节点
newChild, // 本次更新的子ReactElement
lanes, // 优先级相关
);
}
Similar to the reconcileSingleElement method, the reconcileChildrenArray actually tries to reuse the corresponding child nodes on the current tree. If a child node that cannot be reused is encountered, a new node is created; but the difference is that the child node that reconcileChildrenArray needs to process is actually a array, so it is necessary to compare the new array (ReactElement created in this update) with the original array (the corresponding sub-fiber node on the current tree). The general idea is as follows:
- Traverse the new and old array elements according to the index, and compare the old and new arrays one by one. The basis for the comparison is whether the key attributes are the same;
- If the key attributes are the same, the node is reused and the traversal is continued until it encounters a situation that cannot be reused (or all the nodes in the old array have been reused) and the traversal ends.
- If all nodes of the old array have been reused, but the new array has unprocessed parts, a new Fiber node is created based on the unprocessed parts of the new array.
- If the old array has nodes that have not been traversed (that is, the first traversal encounters a situation that cannot be reused and exits halfway), then put this part into a map, and then continue to traverse the new array to see if there is any Find the ones that can be reused in the map; if they can be reused, reuse them, otherwise create a new Fiber node; for the old nodes that are not reused, delete them all (deleteChild).
It should be noted that although the reconcileChildrenArray creates the Fiber nodes of the entire array (newChild), the final return is actually the first Fiber node in the array, in other words: the loop body in the next performUnitOfWork ( unitOfWork) is actually the first Fiber node in this array; and when the "first Fiber node" is executed to the completeWork stage, its sibling - that is, the second Fiber node in the array to As the loop body (unitOfWork) in the next performUnitOfWork.
Optimized work path
The above has spent a lot of space to introduce the main working path of beginWork all the way, let's go back to beginWork:
if (current !== null) {
const oldProps = current.memoizedProps
const newProps = workInProgress.pendingProps
if (
oldProps !== newProps ||
hasLegacyContextChanged() // 判断context是否有变化
) {
/* 该didReceiveUpdate变量代表本次更新中本Fiber节点是否有变化 */
didReceiveUpdate = true
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false
switch (
workInProgress.tag
) {
// 省略
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
} else {
didReceiveUpdate = false
}
} else {
didReceiveUpdate = false
}
The function of the current code segment is to judge whether the current Fiber node has changed. The basis for the judgment is: props and fiber.type (such as the function of the function component, the class of the class component, the html tag, etc.) and the context have not changed ; Under the premise that the fiber node does not change ( !includesSomeLane(renderLanes, updateLanes)
involves priority, we will not discuss it for the time being), try to reuse the sub-fiber node as it is or directly "prune": the bailoutOnAlreadyFinishedWork method.
bailoutOnAlreadyFinishedWork
Next, let's look bailoutOnAlreadyFinishedWork methods:
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 省略
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // 判断子节点中是否需要检查更新
return null; // 剪枝:不需要关注子节点(ReactElement)了
} else {
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
- When
!includesSomeLane(renderLanes, workInProgress.childLanes) === true
, it will directlyreturn null
, which is the "pruning" strategy mentioned above: no longer pay attention to the child nodes under it, and go to the completeWork stage of this node. - When the above conditions are not met, clones the corresponding sub-fiber node on the current tree and returns it as the main body of the next performUnitOfWork.
Clone the corresponding child Fiber nodes on the current tree - cloneChildFibers
The "cloning the corresponding child Fiber node on the current tree" here may cause some confusion. Let's look directly at the code of cloneChildFibers :
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
// 省略
/* 判断子节点为空,则直接返回 */
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child; // 这里怎么会是拿workInProgress.child来充当currentChild呢?解释看下文
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 复用currentChild
workInProgress.child = newChild;
newChild.return = workInProgress; // 让子Fiber节点与当前Fiber节点建立联系
/* 遍历子节点的所有兄弟节点并进行节点复用 */
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
Here we see that the workInProgress.child is used to create a child node. How can it be said to be a clone of the corresponding child Fiber node on the current tree? And it stands to reason that the child Fiber node has not been created at this time, so how can workInProgress.child have a value?
In fact, this is the current node is at beginWork stage parent node by createWorkInProgress created out of the way, it will perform workInProgress.child = current.child
, thus creating your own child node of this node and cover workInProgress.child
before, workInProgress.child
actually point is current.child
.
The following is an example to illustrate:
EffectTag
As mentioned above, in the update scenario, in addition to creating the child Fiber node as in mount, it will also diff with the last rendered child node, so as to find out what kind of DOM operation needs to be performed, and mark it as "marked". "On the newly created child Fiber node, let's introduce this "tag" - EffectTag .
EffectTag is a major innovation of Fiber Reconciler compared to Stack Reconciler. In the past, Stack Reconciler made a commit every time a node was diffed (of course, since Stack Reconciler is executed synchronously, it will not be the browser's turn until all nodes are committed. GUI thread for rendering, so that it will not cause the problem of "only partial update"), and Fiber Recconciler only puts effectTag on the target node after the diff is out, and does not go to the commit stage until all nodes complete the render stage Then it is unified into the commit stage, which realizes the decoupling of reconciler (render stage) and renderer (commit stage) .
Definition of the EffectTag type
The effectTag is actually the DOM operation that needs to be performed on the node (it can also be considered as a side effect, that is, )
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
Why do you need to use binary to represent effectTag?
This is because the same Fiber node may need to perform multiple types of DOM operations, that is, multiple types of effectTags need to be added. At this time, as long as these effectTags are "bitwise OR" (|) operation, they can be aggregated into All effectTag types owned by the current Fiber node.
To determine whether a Fiber node has a certain type of effectTag, it is actually very simple. Take fiber.effectTag and the binary value corresponding to this type of effectTag to do the "bitwise AND" (&) operation, and then according to the operation result Whether it is NoEffect(0)
is enough.
renderer performs DOM operations based on EffectTag
Take the renderer "judging whether the current node needs to be inserted into the DOM" as an example:
- fiber.stateNode exists, that is, the corresponding DOM node is saved in the Fiber node
- (fiber.effectTag & Placement) !== 0, that is, the Fiber node has a Placement effectTag.
The above is a good understanding of the update operation, but what should I do with the mountChildFibers called in reconcileChildren during mount?
The fiber.stateNode during mount is null, so the DOM insertion operation will not be performed?
fiber.stateNode
will be created in the "return" phase of the node, that is, completeWork.
Will there be Placement EffectTag
on each node when mounted?
Assuming that mountChildFibers also assigns effectTag, it is foreseeable that all nodes of the entire Fiber tree will have Placement effectTag
. Then in the commit phase, each node will perform an insertion operation when performing DOM operations, so a large number of DOM operations are extremely inefficient.
In order to solve this problem, only FiberRootNode will be assigned Placement effectTag
during mount, and only one insert operation will be performed in the commit phase.
Let's go back to reconcileChildren method and set a breakpoint at the position shown in the figure below, then refresh the page to see if it will go to the reconcileChildFibers position when the first screen is rendered:
Then, we can see the following breakpoint results: the current workInProgress input parameter is actually FiberRootNode , which is the DOM element mounted by the <App />
component ( ReactDOM.render(<App />, document.getElementById('root'))
); and the current current input parameter is not empty, Therefore, we will come to this code segment that is usually only executed by update; and when we resume the code execution, the first screen has been rendered, and it has not stopped at the breakpoint position again. Therefore, in mount (first screen rendering) , only FiberRootNode will "track side effects" ( shouldTrackSideEffects === true
), that is, marked with EffectTag.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。