概览
React-render阶段一
解释了当组件进入reconciler
后的执行过程, 从root
节点开始调度, 循环调用beginWork
创建子节点. 其中创建子节点的过程又分为挂载阶段和更新阶段, 挂载阶段不追踪副作用, 更新阶段追踪副作用, 更新阶段又分为可复用和不可复用, 可复用的会进入bailout
的复用逻辑, 会把current
树中的当前节点以及其子节点复制到workInProgress
树中, 没有进入bailout
阶段的Fiber
节点会进入diff
算法(对应的current
中的Fiber
节点与返回的JSX
对比, 生成新的Fiber
节点), 并为新的Fiber
节点打上effectTag
当前Fiber
节点没有子节点时就进入了completeWork
, 可以理解为递归阶段的归阶段, completeWork
的目的就是为了创建好对应的dom
节点插入对应的父级节点的dom
节点, 为其添加副作用标识, 再commit
阶段将对应的节点展示到页面上并执行对应的副作用.
以下我以cra
创建的项目的代码举例, js如下
function App() {
const [num, setNum] = useState(0)
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={() => { setNum(num + 1) }}>
{num} <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
复制代码
CompleteWork执行时机与分析
completeWork
发生在当前Fiber
节点没有子节点的情况下, 源码发生在performUnitOfWork
函数中, 这个函数发生在上文提到过的workLoopSync
中, 这个函数将被循环调用
上图打了两个断点处就是completeWork是否执行的条件, 第一个断点拿到的是beginWork
创建好的子Fiber
节点, 如果没有子Fiber
节点则返回null
, 只有当next
为null
的时候才会进入completeWork
, completeWork
的开始源于他的上层函数completeUnitOfWork
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork; // 获取当前completeWork的Fiber节点
do {
var current = completedWork.alternate; // 获取当前completeWork对应的current树上的节点, 没有则表示是新增的节点
var returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentFiber(completedWork);
var next = void 0;
if ( (completedWork.mode & ProfileMode) === NoMode) {
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, subtreeRenderLanes); // Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentFiber();
if (next !== null) {
workInProgress = next;
return;
}
} else {
var _next = unwindWork(completedWork, subtreeRenderLanes); // Because this fiber did not complete, don't reset its expiration time.
if (_next !== null) {
_next.flags &= HostEffectMask;
workInProgress = _next;
return;
}
if ( (completedWork.mode & ProfileMode) !== NoMode) {
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); // Include the time spent working on failed children before continuing.
var actualDuration = completedWork.actualDuration;
var child = completedWork.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
}
if (returnFiber !== null) {
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
}
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
} // Otherwise, return to the parent
completedWork = returnFiber; // Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null); // We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
复制代码
completeUnitOfWork
从源码中可以看到是一个do while
循环, 终止条件有completeWork !== null
或者循环内return
前的几个终止条件, 我们可以看到有一个是siblingFiber
不为null
的情况. 即当前的节点存在兄弟节点时并且已经没有子节点, 当前节点会结束completeWork
, 跳出调用栈, 执行下一次循环, 进入兄弟节点的beginWork
.当兄弟节点为null
的时候, 那么completeWork
会被赋值为returnFiber
, 这个时候注意并没有用return
跳出调用栈, 因为父级节点的beginWork
已经被执行, 因此会进入父级节点的completeWork
, 由此向上, 当completeWork
为null
时意味着归到根节点
接下来分析一下completeWork
做的具体的事情
function completeWork(current, workInProgress, renderLanes) {
console.log('completeWork', 'tag:', workInProgress.tag, ' type:', workInProgress.type);
var newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent:
{
var Component = workInProgress.type;
if (isContextProvider(Component)) {
popContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
if (!newProps) {
if (!(workInProgress.stateNode !== null)) {
{
throw Error( "We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue." );
}
} // This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
var currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
var _wasHydrated = popHydrationState(workInProgress);
if (_wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef$1(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
// ...以下省略的是针对其他组件的执行逻辑, 这里我们重点关注前几个
}
复制代码
上面的代码中看到completeWork
函数就是针对不同的Fiber
节点的Tag
, 处理不同的逻辑, 我们根据p
标签为例, 会进入case
为HostComponent
的分支
// completeWork顺序如下(展示一部分, 归到p标签为止)
1. img
2. {num}
3. num后面的空格
4. code
5. and save to reload.
6. p
复制代码
mount阶段
根据上面的completeWork
的分析, 我们直接看上面 HostComponent
的逻辑, 一行一行看
开始的逻辑都是一样的, 首先处理context
, 获取根容器
popHostContext
是和context
相关的逻辑, 暂时跳过rootContainerInstance
是获取根容器<div id="root"></div>
接下来的逻辑会根据挂载和更新进入不同的条件语句, 重新贴一下核心代码
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
if (!newProps) { // 如果没有新的props并且stateNode为null, 可能是React发生了内部错误, 挂载时newProps至少也是一个{}, 一定不会进这里
if (!(workInProgress.stateNode !== null)) {
{
throw Error( "We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue." );
}
} // This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
var currentHostContext = getHostContext(); // context相关
var _wasHydrated = popHydrationState(workInProgress); // 服务端渲染相关
if (_wasHydrated) {// 服务端渲染
if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
markUpdate(workInProgress);
}
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef$1(workInProgress);
}
}
复制代码
跳过context
相关和服务端渲染相关, 会进入 instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress)
, 这里主要是创建dom
实例, 进去看下这个函数
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace;
{
var hostContextDev = hostContext;
// 检测dom是否正确嵌套
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (typeof props.children === 'string' || typeof props.children === 'number') {
var string = '' + props.children;
var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
}
// 创建dom实例
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
// 缓存这个fiber节点
precacheFiberNode(internalInstanceHandle, domElement);
// 更新fiber节点的props, react会自己定义一个值, 所有的props将存放在当前的dom实例上
updateFiberProps(domElement, props);
return domElement;
}
复制代码
返回创建好的domElement
, 然后直接插入逻辑, 对应的代码为appendAllChildren(instance, workInProgress, false, false);
, 看下这个函数
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
// 拿到当前工作单元的child Fiber节点, 即拿到第一个子节点
var node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) { // 如果是文本节点或者是原生dom节点
// 这个函数调用的就是parent.appendChild(node.stateNode);
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) ; else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) { // 如果是当前工作单元, 插入完毕
return;
}
while (node.sibling === null) { // 没有兄弟节点则Fiber向上冒泡
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return; // 把兄弟节点的return节点赋值给父节点
node = node.sibling; // 把node赋值为兄弟节点
}
};
复制代码
appendAllChildren
的作用和函数名相同, 目的就是把当前工作单元的所有子节点全部插入到刚创建好的dom实例
中, 全部插入完毕执行workInProgress.stateNode = instance;
, 这里采用的是深度优先遍历的方式
此时这里的instance
为插入完的dom
实例, 并把对应的节点赋值到当前Fiber
节点的stateNode
上
然后执行的是finalizeInitialChildren
方法, 此方法调用了setInitialProperties
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
var isCustomComponentTag = isCustomComponent(tag, rawProps);
{
validatePropertiesInDevelopment(tag, rawProps);
} // TODO: Make sure that we check isMounted before firing any of these events.
var props;
switch (tag) {
// 跳过一些dom节点的判断逻辑
default:
props = rawProps;
}
// 判断props是否合法
assertValidProps(tag, props);
// 设置初始化的dom属性
setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);
switch (tag) {
case 'input':
track(domElement);
postMountWrapper(domElement, rawProps, false);
break;
case 'textarea':
track(domElement);
postMountWrapper$3(domElement);
break;
case 'option':
postMountWrapper$1(domElement, rawProps);
break;
case 'select':
postMountWrapper$2(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}
break;
}
}
复制代码
上面的函数主要是判断了props
是否合法, 并对特殊的dom
节点做了一些操作, 并把初始化的属性赋值到当前的dom
上
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
for (var propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
var nextProp = nextProps[propKey];
if (propKey === STYLE) { // style的时候
{
if (nextProp) {
Object.freeze(nextProp);
}
}
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // dangerouslySetInnerHTML
var nextHtml = nextProp ? nextProp[HTML$1] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) { // children
if (typeof nextProp === 'string') { // 如果节点是字符串
var canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') { // 如果是数字就转换为字符串
setTextContent(domElement, '' + nextProp);
}
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if ( typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
复制代码
复制初始化的props
是调用了setInitialDOMProperties
, 这个函数循环调用了新的props
, 并对每个propKey
做了特定的赋值操作, 这一步主要在setValueForProperty
中, 这一步会调用node.setAttribute
来为创建好的dom
元素设置属性
继续走下去进行的是判断是否存在ref
, 如果存在ref
则调用markRef$1(workInProgress);
函数
最后执行bubbleProperties
if ( (completedWork.mode & ProfileMode) !== NoMode) {
var actualDuration = completedWork.actualDuration;
var treeBaseDuration = completedWork.selfBaseDuration;
var child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes));
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
actualDuration += child.actualDuration;
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
复制代码
bubbleProperties
根据fiber.child
及fiber.child.sibling
更新subtreeFlags
和childLanes
, 主要是为了标记子树有没有更新, 这样可以通过 fiber.subtreeFlags
快速判断子树是否有副作用钩子,不需要深度遍历. 在React17版本后
使用subtreeFlags
替换了finishWork.firstEffect
的副作用链表, 操作主要发生在bubbleProperties
函数中, 核心代码如下
update阶段
当进入update
阶段, 假如我们把p
节点的Fiber
作为案例, 对应case为HostComponent
会进入以下的条件分支
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
}
复制代码
updateHostComponent$1代码如下
updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
var oldProps = current.memoizedProps; // 此状态为更新, 获取current的props
if (oldProps === newProps) { // 判断props是否相同, 相同表示进入了bailout阶段,哪怕children变了我们也不需要做什么操作,因此直接跳过
return;
}
var instance = workInProgress.stateNode; // 获取实例
var currentHostContext = getHostContext();
var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext);
workInProgress.updateQueue = updatePayload;
if (updatePayload) {
// 标记更新, 内部直接设置workInProgress.flags |= Update
markUpdate(workInProgress);
}
};
复制代码
这里调用了一个主要的更新方法为prepareUpdate
, 返回的updatePayload
将被加入工作单元的更新队列中, 这个函数调用了diffProperties
, 其中返回的updatePayload
是一个数组, 第i项是对应的propKey
, 第i + 1
项是对应的value
, 当存在updatePayload
的时候意味着这个HostComponent
存在增,或者更新的情况, 会调用markUpdate
进行更新
function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {
{
validatePropertiesInDevelopment(tag, nextRawProps);
}
var updatePayload = null;
var lastProps;
var nextProps;
switch (tag) {
// 这里省略了对特性的dom标签比如(input, select等)赋值lastProps和nextProps的过程
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function') {
trapClickOnNonInteractiveElement(domElement);
}
break;
}
// 检验props
assertValidProps(tag, nextProps);
var propKey;
var styleName;
var styleUpdates = null;
for (propKey in lastProps) {
// 针对删除的情况, 需要标记对应的propKey为null
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
continue;
}
if (propKey === STYLE) {
var lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) ; else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (!updatePayload) {
updatePayload = [];
}
} else {
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps != null ? lastProps[propKey] : undefined;
// 针对新增或者更新的情况, 需要标记对应的propKey为null
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) { // 对应props被删除的情况
continue;
}
if (propKey === STYLE) {
{
if (nextProp) {
Object.freeze(nextProp);
}
}
if (lastProp) {
// 在 `lastProp` 上取消设置样式,但不在 `nextProp` 上设置.
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
} // 从lastProps中的style更新数据.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var nextHtml = nextProp ? nextProp[HTML$1] : undefined;
var lastHtml = lastProp ? lastProp[HTML$1] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, nextHtml);
}
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string' || typeof nextProp === 'number') {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if ( typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
if (!updatePayload && lastProp !== nextProp) {
updatePayload = [];
}
} else if (typeof nextProp === 'object' && nextProp !== null && nextProp.$$typeof === REACT_OPAQUE_ID_TYPE) {
nextProp.toString();
} else {
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
{
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;
}
复制代码
执行完这个diffProperties
, 再执行bubbleProperties(workInProgress)
, 然后就结束了当前节点的completeWork
注意点
Fiber
的tag
为function
的时候是不会进入completeWork
的- 挂载的时候插入的
dom
节点的获取方式在于完成的finishedWork
, 在performSyncWorkOnRoot
函数中 React17之前
原本有一根finishWork.firstEffect
开始的副作用链表, 始终指向的是第一个产生副作用的链表, 链表的nextEffect
指向的是下一个具有副作用的链表, 这根链表在React17版本后
使用subtreeFlags
替换了, 操作主要发生在bubbleProperties
函数中
全流程
这里梳理一下上述所说的全流程