修改app.js代码,将num放入code标签的title属性中。
import "./App.css"
import { useState } from "react"
function App() {
const [num, setNum] = useState(0)
const handleNum = () => {
let _num = num + 1
setNum(_num)
}
return (
<div className="App">
<header className="App-header">
<p onClick={handleNum}>
<code title={num}>{num}</code>
</p>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
Learn React
</a>
</header>
</div>
)
}
export default App
初次渲染
在首屏渲染中,comleteWork,根据不同的tag进入不同的component中,code的tag为5,进入hostComponent中,因为current为null,会进入createInstance,创建新的dom元素。
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps; // Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
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:
...
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 new 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;
}
...
}
}
触发更新
updateHostComponent$1
触发更新时,current不为null,进入 updateHostComponent$1中
prepareUpdate
diffProperties
function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {
{
validatePropertiesInDevelopment(tag, nextRawProps);
}
var updatePayload = null;
var lastProps;
var nextProps;
//根据不同的tag进入不同逻辑,当前tag为code不为表单属性,进入default
switch (tag) {
case 'input':
lastProps = getHostProps(domElement, lastRawProps);
nextProps = getHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'select':
lastProps = getHostProps$1(domElement, lastRawProps);
nextProps = getHostProps$1(domElement, nextRawProps);
updatePayload = [];
break;
case 'textarea':
lastProps = getHostProps$2(domElement, lastRawProps);
nextProps = getHostProps$2(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}
break;
}
//检验属性合法性
assertValidProps(tag, nextProps);
var propKey;
var styleName;
var styleUpdates = null;
//遍历上一次属性
for (propKey in lastProps) {
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)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the allowed property list in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
//遍历下一次属性
for (propKey in nextProps) {
//对应的值
var nextProp = nextProps[propKey];
var lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
continue;
}
if (propKey === STYLE) {
{
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
} // Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
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) {
// We eagerly listen to this even though we haven't committed yet.
if ( typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else {
// For any other property we always add it to the queue and then we
// filter it out using the allowed property list during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
{
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;
} // Apply the diff.
for (propKey in lastProps)
lastProps有两个属性,一个是title,一个是children,其中值都为0
终止的情况是,nextProps存在propKey或者lastProps不存在propKey或者 lastProps[propKey] == null,下方的循环争对lastProps的propKey对应的value被删除的情况,否则跳出for循环。
因为code的两个属性的propKey没有被删除,因此跳出循环。
for (propKey in nextProps)
更新时,num的值为1,nextProps也有两个属性,一个是title,一个是children,因此值都为1
终止的情况是,nextProps不存在propKey)或者nextProp === lastProp或者nextProp == null && lastProp == null,争对的是更新或者新增的情况,否则跳出循环。
循环中,根据propKey的不同,进入不同的逻辑中。
- title属性
由于title属性不属于这些属性,进入默认的处理逻辑。title处理完后,tilte属性会赋值给一个数组,数组第i项时propKey,第i+1项是对应的propsKey的value,即nextProp
- children属性
diffProperties返回的数组,返回一个第i项是属性,第i+1项是值的数组。
prepareUpdate的数组返回给workInProgress.updateQueue。当updatePayload存在的情况下,进入markUpdate中。
markUpdate:将workInProgress的flags增加一个update的属性
function markUpdate(workInProgress) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.flags |= Update;
}
completeUnitOfWork:生成包含effectTag的链表
根据上图的call Stack中,执行完completeWork后进入performSyncWorkOnRoot中,进入commitRoot,最终进入commit阶段。
function completeUnitOfWork(unitOfWork) {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
var completedWork = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
//当前节点
var current = completedWork.alternate;
//当前节点的父级 对应的FIber节点
var returnFiber = completedWork.return; // Check if the work completed or if something threw.
//如果当前完成节点的兄弟节点在执行“递”或“归”阶段抛出错误时,
//表示这颗子树没有完成“递”与“归”的阶段,所以这个父节点就会被标记成Incomplete。
//这些子树不包含Incomplete,表示这颗子树顺利完成了“递”与“归”的阶段。
//此时需要将包含effectTag的节点挂载到returnFiber上(此处与视频源代码存在出处)
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) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
} else {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
var _next = unwindWork(current, completedWork); // Because this fiber did not complete, don't reset its lanes.
if (_next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
_next.flags &= HostEffectMask;
workInProgress = _next;
return;
}
if ( (completedWork.mode & ProfileMode) !== NoMode) {
// Record the render duration for the fiber that errored.
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) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
} else {
// We've unwound all the way to the root.
workInProgressRootExitStatus = RootDidNotComplete;
workInProgress = null;
return;
}
}
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 === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
如果将RootFiber比喻一颗圣诞树,圣诞树上的灯表示一个FIber节点,每一次更新时,某些Fiber节点会标记上一些effectTag的标记,表示更新操作。
更新时是否需要像初始化渲染时,采用深度优先的方式,遍历整棵Fiber树,来找到包含effectTag的节点吗?
可行但效率低。如果一个节点包含Efag Tag,会与其他包含effectTag的Fiber节点,组成一个链表,就像图中连接红色灯的黄色灯带一样。当整个render阶段完成时,就形成一个连接所有Efag Tag的完整的链表。因此更新时,不需要像初次渲染时对root Fiber树广度遍历,只需遍历这条链表,就可以找到包含effectTag的节点。
returnFiber(当前节点的父级 对应的FIber节点)
completedWork表示当前完成的Fiber节点。
//视频中此段代码
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
p标签不为null,且p标签的effectTag按位与Incomplete的值不等于NoFlags。如果当前完成节点的兄弟节点在执行“递”或“归”阶段抛出错误时,表示这颗子树没有完成“递”与“归”的阶段,所以这个父节点就会被标记成Incomplete。这些子树不包含Incomplete,表示这颗子树顺利完成了“递”与“归”的阶段,此时需要将包含effectTag的节点挂载到EffectTag的链表上。
1.code
第一个进入completeUnitOfWork的Firber节点是code,code的firstEffct为null,继续往下,判断当前completeWork是否包含effectTag(code的Tiltle和children发生了变化),包含进入下面returnFiber.firstEffect=completedWork.firstEffect,returnFiber.lastEffect=completedWork.lastEffect,即p的firstEffct赋值给当前的code,p的lastEffect赋值给当前的code。
2.p
下一个是p,p的returnFiber是header,header的firstEffect为null,则returnFiber.firstEffect=completedWork.firstEffect,即header的firstEffect赋值给p。继续,completedWork.lastEffect !== null,当前节点的lastEffect不等于null,继续,
header.lastEffect为null,returnFiber.lastEffect = completedWork.lastEffect,header的lastEffect等于p的lastEffect,继续
p的effect是否包含Effect,原因?
因为P的props有一个箭头函数onClick,接受一个箭头函数,所以每一次render时,他的onClick的值都是不同的箭头函数,所以p的属性都有变化,所以包含Effct
....
const handleNum = () => {
let _num = num + 1
setNum(_num)
}
<p onClick={handleNum}>
<code title={num}>{num}</code>
</p>
.....
header的lastEffect不为null,将header的lastEffct的nextEffect赋值给p,header的lastEffect赋值给p
3.a
下一个是a,header的firstEffect不为null,跳过;a的lastEffect等于null,跳过;a的内容不变,不包含effct,跳过。
4.header
div的firstEffect为null,div的firstEffect等于header的firstEffect;header的lastEffect不等于null,div的lastEffect等于null。div的lastEffect等于header的lastEffect(即p)。header不包含effect。
由此可以看出,整个流程中,当到达根节点,根节点的firstEffect指向的是第一个包含effect的叶子节点,此处为code,而code的nextEffect指向的是P。在本次更新中,只有code与p存在effctTag。如果有其他fiber节点的effct存在,p的nectEffect又指向的是下一个含Effect的Fiber 节点。
finishWork存在firstEffect,他的firstEffect是code,code的nextEffect是p,p的nectEffect为null。
所以本次更新只有code和p包含effect,
结合,这就是render 阶段的完整流程。