修改app.js代码,将num放入code标签的title属性中。
import "./App.css"import { useState } from "react"function App() {const [num, setNum] = useState(0)const handleNum = () => {let _num = num + 1setNum(_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 callbackmarkRef$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不为表单属性,进入defaultswitch (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 parentcompletedWork = 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 + 1setNum(_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 阶段的完整流程。
