You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * https://github.com/reactjs/rfcs/pull/107 * @param {*} type jsx 标签的类型 * @param {object} props 属性 * @param {string} key 用于优化的 key */functionjsx(type,config,maybeKey){letpropName;// 去除了保留字段后存的 props 对象constprops={};letkey=null;letref=null;if(maybeKey!==undefined){// ... 开发模式的一些检查key=''+maybeKey;}// 处理 jsx 有展开运算符的情况if(hasValidKey(config)){// ... 开发模式的一些检查key=''+config.key;}if(hasValidRef(config)){ref=config.ref;}// 除了内部保留 Props 外的所有属性都构造到 props 对象里for(propNameinconfig){if(hasOwnProperty.call(config,propName)&&!RESERVED_PROPS.hasOwnProperty(propName)){props[propName]=config[propName];}}// 处理 defaultPropsif(type&&type.defaultProps){constdefaultProps=type.defaultProps;for(propNameindefaultProps){if(props[propName]===undefined){props[propName]=defaultProps[propName];}}}// 生成 React ElementreturnReactElement(type,key,ref,undefined,undefined,ReactCurrentOwner.current,props,);}functionReactElement(type,key,ref,self,source,owner,props){constelement={// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,// Built-in properties that belong on the element
type,
key,
ref,
props,// Record the component responsible for creating this element._owner: owner,};// ...returnelement;}
可以看到,jsx 的执行结果就是 React Element,而 React Element 就是包含了 jsx 标签的类型、属性、key、ref 的对象。
// Use this function to schedule a task for a root. There's only one task per// root; if a task was already scheduled, we'll check to make sure the priority// of the existing task is the same as the priority of the next level that the// root has work on. This function is called on every update, and right before// exiting a task.functionensureRootIsScheduled(root: FiberRoot,currentTime: number){// callbackNode 存的是当前正在调度的 Schedule TaskconstexistingCallbackNode=root.callbackNode;// Check if any lanes are being starved by other work. If so, mark them as// expired so we know to work on those next.// 为待处理的每个 Lane 计算过期时间,如果已有过期时间则判断有没有过期// 如果已过期就记录在 root.expiredLanes 里markStarvedLanesAsExpired(root,currentTime);// Determine the next lanes to work on, and their priority.// 确定要处理的下一批优先级// mount 时是 defaultLaneconstnextLanes=getNextLanes(root,root===workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);// 没有 Lanes 待处理if(nextLanes===NoLanes){// Special case: There's nothing to work on.if(existingCallbackNode!==null){cancelCallback(existingCallbackNode);}root.callbackNode=null;root.callbackPriority=NoLane;return;}// We use the highest priority lane to represent the priority of the callback.// 挑出待处理优先级里的最高优先级// (只有当 nextLanes 包含纠缠Lanes 或是 TransitionLanes 或 RetryLanes 时才会再次挑出不一样的 Lane)constnewCallbackPriority=getHighestPriorityLane(nextLanes);// Check if there's an existing task. We may be able to reuse it.// callbackPriority 存的是正在调度任务的优先级constexistingCallbackPriority=root.callbackPriority;// 优先级相同则无需重新调度,不同那么 priority 只会更高if(existingCallbackPriority===newCallbackPriority){// The priority hasn't changed. We can reuse the existing task. Exit.return;}// 有更高优先级任务,现有任务则取消if(existingCallbackNode!=null){// Cancel the existing callback. We'll schedule a new one below.cancelCallback(existingCallbackNode);}// Schedule a new callback.// 调度一个新的更新letnewCallbackNode;// 同步执行优先级if(newCallbackPriority===SyncLane){// ...// 让处理函数 performSyncWorkOnRoot 进入一个队列。// PS:performSyncWorkOnRoot 处理的内容与 performConcurrentWorkOnRoot 处理内容是一样的,// 只不过一个是同步执行,一个是并发执行。具体源码等讲完 performConcurrentWorkOnRoot 后再来看。// 目前可以理解 performSyncWorkOnRoot 处理的内容就是序列化生成新的完整的 Fiber 树,然后渲染到真实视图上。scheduleSyncCallback(performSyncWorkOnRoot.bind(null,root));if(supportsMicrotasks){// Flush the queue in a microtask.// ...// 调度一个微任务执行队列内所有回调scheduleMicrotask(()=>{// In Safari, appending an iframe forces microtasks to run.// https://github.com/facebook/react/issues/22459// We don't support running callbacks in the middle of render// or commit so we need to check against that.if((executionContext&(RenderContext|CommitContext))===NoContext){// Note that this would still prematurely flush the callbacks// if this happens outside render or commit phase (e.g. in an event).flushSyncCallbacks();}});}else{// Flush the queue in an Immediate task.scheduleCallback(ImmediateSchedulerPriority,flushSyncCallbacks);}newCallbackNode=null;}else{// Lane 优先级转 Scheduler 优先级letschedulerPriorityLevel;switch(lanesToEventPriority(nextLanes)){caseDiscreteEventPriority:
schedulerPriorityLevel=ImmediateSchedulerPriority;break;caseContinuousEventPriority:
schedulerPriorityLevel=UserBlockingSchedulerPriority;break;caseDefaultEventPriority:
// Mount 时走这里,中等优先级schedulerPriorityLevel=NormalSchedulerPriority;break;caseIdleEventPriority:
schedulerPriorityLevel=IdleSchedulerPriority;break;default:
schedulerPriorityLevel=NormalSchedulerPriority;break;}// Scheduler 模块进行调度newCallbackNode=scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null,root),);}root.callbackPriority=newCallbackPriority;// 正在调度的优先级root.callbackNode=newCallbackNode;// Schedule Task}
// 每个时间分片任务的入口functionperformConcurrentWorkOnRoot(root,didTimeout){// ...// 拿到正在调度的 Schedule TaskconstoriginalCallbackNode=root.callbackNode;// ...// TODO: This was already computed in the caller. Pass it as an argument.letlanes=getNextLanes(root,root===workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);// 是否用时间分片去执行的标志位constshouldTimeSlice=!includesBlockingLane(root,lanes)&&// 某些高优先级的 Lanes 禁用并发!includesExpiredLane(root,lanes)&&// 过期 Lanes 禁用并发// didTimeout 是为了解决饥饿问题// 比如一个低优先级的 work 不断被打断,到过期时间之后,这个字段为 true,// 意味着我等待了太久时间,等不及并发执行了(disableSchedulerTimeoutInWorkLoop||!didTimeout);letexitStatus=shouldTimeSlice
? renderRootConcurrent(root,lanes)// 并发
: renderRootSync(root,lanes);// 同步// 判断 root 的更新是否处理完了// 如果未处理完 exitStatus === RootInProgress// 如果处理完了或报异常了 exitStatus !== RootInProgressif(exitStatus!==RootInProgress){// ... 异常情况处理// 更新处理完了constfinishedWork: Fiber=(root.current.alternate: any);root.finishedWork=finishedWork;root.finishedLanes=lanes;finishConcurrentRender(root,exitStatus,lanes);}// 每执行完一个切片时间,继续开启调度,处理有没有被高优先级任务打断ensureRootIsScheduled(root,now());if(root.callbackNode===originalCallbackNode){// The task node scheduled for this root is the same one that's// currently executed. Need to return a continuation.// 只是切片的时间到了,没有被高优先级任务打断,继续执行该 callbackreturnperformConcurrentWorkOnRoot.bind(null,root);}returnnull;}
functionrenderRootConcurrent(root: FiberRoot,lanes: Lanes){constprevExecutionContext=executionContext;executionContext|=RenderContext;// 标志已进入 Render 阶段// If the root or lanes have changed, throw out the existing stack// and prepare a fresh one. Otherwise we'll continue where we left off.// 如果 root 或 lanes 改变了,那么需要创建一个新的 WIP 树if(workInProgressRoot!==root||workInProgressRootRenderLanes!==lanes){// ...prepareFreshStack(root,lanes);}do{try{// 开启一个 loop 持续构建 Fiber 树,直到时间分片时间不够workLoopConcurrent();break;}catch(thrownValue){// ...}}while(true);// ... // 标志 Render 阶段结束executionContext=prevExecutionContext;// 检查 WIP 是否处理完了.if(workInProgress!==null){// ...// 代表 WIP 还未处理完,只是时间分片时间到了returnRootInProgress;}else{// WIP 已处理完// ...// Set this to null to indicate there's no in-progress render.// 置空标志位workInProgressRoot=null;workInProgressRootRenderLanes=NoLanes;// Return the final exit status.returnworkInProgressRootExitStatus;}}
先来看看是怎样生成一个 WIP 树的:
functionprepareFreshStack(root: FiberRoot,lanes: Lanes): Fiber{root.finishedWork=null;root.finishedLanes=NoLanes;if(workInProgress!==null){// ...}workInProgressRoot=root;// 创建一个与 root.current 互相引用的 Fiber 节点 (rootWorkInProgress)constrootWorkInProgress=createWorkInProgress(root.current,null);// 标志位处理workInProgress=rootWorkInProgress;workInProgressRootRenderLanes=renderLanes=lanes;workInProgressIsSuspended=false;workInProgressThrownValue=null;workInProgressRootDidAttachPingListener=false;workInProgressRootExitStatus=RootInProgress;workInProgressRootFatalError=null;workInProgressRootSkippedLanes=NoLanes;workInProgressRootInterleavedUpdatedLanes=NoLanes;workInProgressRootRenderPhaseUpdatedLanes=NoLanes;workInProgressRootPingedLanes=NoLanes;workInProgressRootConcurrentErrors=null;workInProgressRootRecoverableErrors=null;// 会处理更新队列,处理所有入队的 Update,把相同 Fiber 的 Update 串联成链表,// 把每个 Fiber 的优先级层层冒泡给 parent.childLanes 直到处理到 HostFiberfinishQueueingConcurrentUpdates();returnrootWorkInProgress;}// This is used to create an alternate fiber to do work on.exportfunctioncreateWorkInProgress(current: Fiber,pendingProps: any): Fiber{letworkInProgress=current.alternate;if(workInProgress===null){// We use a double buffering pooling technique because we know that we'll// only ever need at most two versions of a tree. We pool the "other" unused// node that we're free to reuse. This is lazily created to avoid allocating// extra objects for things that are never updated. It also allow us to// reclaim the extra memory if needed.// 创建 fiberworkInProgress=createFiber(current.tag,pendingProps,current.key,current.mode,);workInProgress.elementType=current.elementType;workInProgress.type=current.type;workInProgress.stateNode=current.stateNode;// 建立与 current 的相互引用workInProgress.alternate=current;current.alternate=workInProgress;}else{// ...}// Reset all effects except static ones.// Static effects are not specific to a render.workInProgress.flags=current.flags&StaticMask;workInProgress.childLanes=current.childLanes;workInProgress.lanes=current.lanes;workInProgress.child=current.child;workInProgress.memoizedProps=current.memoizedProps;workInProgress.memoizedState=current.memoizedState;workInProgress.updateQueue=current.updateQueue;// Clone the dependencies object. This is mutated during the render phase, so// it cannot be shared with the current fiber.constcurrentDependencies=current.dependencies;workInProgress.dependencies=currentDependencies===null
? null
: {lanes: currentDependencies.lanes,firstContext: currentDependencies.firstContext,};// These will be overridden during the parent's reconciliationworkInProgress.sibling=current.sibling;workInProgress.index=current.index;workInProgress.ref=current.ref;returnworkInProgress;}
这里最关键的是调用 reconcileChildren 生成新的 Fiber 节点并挂在到 WIP.child 下以及把子节点返回出去继续处理。 无论是哪个 tag,几乎都会走这两个步骤,只不过在走这两步前会做一些不同 tag 特殊的逻辑,比如像 HostRoot、HostComponent 就是直接找出要处理的 JSX 对象,像函数式组件 tag 会额外执行 hook 和函数,像类组件 tag 就会实例化执行 render 方法等。 接下来看看 reconcileChildren 具体是怎么创建 Fiber 的
functionreconcileChildren(current: Fiber|null,workInProgress: Fiber,nextChildren: any,renderLanes: Lanes,){if(current===null){// If this is a fresh new component that hasn't been rendered yet, we// won't update its child set by applying minimal side-effects. Instead,// we will add them all to the child before it gets rendered. That means// we can optimize this reconciliation pass by not tracking side-effects.// 对于 mount 的组件 创建新 Fiber 节点挂载在 wip 树workInProgress.child=mountChildFibers(workInProgress,null,nextChildren,renderLanes,);}else{// If the current child is the same as the work in progress, it means that// we haven't yet started any work on these children. Therefore, we use// the clone algorithm to create a copy of all the current children.// If we had any progressed work already, that is invalid at this point so// let's throw it out.// 如有 current 节点,则 diff current ,根据 diff 结果复用(生成)新节点workInProgress.child=reconcileChildFibers(workInProgress,current.child,nextChildren,renderLanes,);}}
exportconstreconcileChildFibers=ChildReconciler(true);exportconstmountChildFibers=ChildReconciler(false);functionChildReconciler(shouldTrackSideEffects){// 省略一系列方法 ...functionplaceSingleChild(newFiber: Fiber): Fiber{// This is simpler for the single child case. We only need to do a// placement for inserting new children.if(shouldTrackSideEffects&&newFiber.alternate===null){newFiber.flags|=Placement|PlacementDEV;}returnnewFiber;}functiondeleteChild(returnFiber: Fiber,childToDelete: Fiber): void{if(!shouldTrackSideEffects){// Noop.return;}// 挂载在父 Fiber 的 deletions 里,打上 ChildDeletion 标志constdeletions=returnFiber.deletions;if(deletions===null){returnFiber.deletions=[childToDelete];returnFiber.flags|=ChildDeletion;}else{deletions.push(childToDelete);}}functiondeleteRemainingChildren(returnFiber: Fiber,currentFirstChild: Fiber|null,): null{if(!shouldTrackSideEffects){// Noop.returnnull;}letchildToDelete=currentFirstChild;while(childToDelete!==null){deleteChild(returnFiber,childToDelete);childToDelete=childToDelete.sibling;}returnnull;}functionreconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber|null,element: ReactElement,lanes: Lanes,): Fiber{constkey=element.key;letchild=currentFirstChild;while(child!==null){// key 相同if(child.key===key){constelementType=element.type;if(// type 相同child.elementType===elementType// ...){// 标记 current child 的兄弟节点为待删除deleteRemainingChildren(returnFiber,child.sibling);// 复用当前 current child 节点constexisting=useFiber(child,element.props);coerceRef(returnFiber,child,existing,element);existing.return=returnFiber;returnexisting;}// type 不同// current child 节点及其兄弟节点为待删除deleteRemainingChildren(returnFiber,child);break;}else{// 标记删除当前 child,继续尝试找出 key、type 相同的兄弟节点deleteChild(returnFiber,child);}child=child.sibling;}// 根据 element 创建新 Fiber 对象constcreated=createFiberFromElement(element,returnFiber.mode,lanes);coerceRef(returnFiber,currentFirstChild,created,element);created.return=returnFiber;returncreated;}functionreconcileChildFibers(returnFiber: Fiber,// 父 Fiber 节点currentFirstChild: Fiber|null,// 当前第一个 Fiber 子节点newChild: any,// 新子节点 -- React Elementlanes: Lanes,): Fiber|null{// This function is not recursive.// If the top level item is an array, we treat it as a set of children,// not as a fragment. Nested arrays on the other hand will be treated as// fragment nodes. Recursion happens at the normal flow.// Handle top level unkeyed fragments as if they were arrays.// This leads to an ambiguity between <>{[...]}</> and <>...</>.// We treat the ambiguous cases above the same.constisUnkeyedTopLevelFragment=typeofnewChild==='object'&&newChild!==null&&newChild.type===REACT_FRAGMENT_TYPE&&newChild.key===null;if(isUnkeyedTopLevelFragment){newChild=newChild.props.children;}// Handle object typesif(typeofnewChild==='object'&&newChild!==null){// 单点 Diffswitch(newChild.$$typeof){caseREACT_ELEMENT_TYPE:
returnplaceSingleChild(// reconcileSingleElement 为新 element 创建对应 Fiber,并串联进 wipreconcileSingleElement(returnFiber,currentFirstChild,newChild,lanes,),);//...}// 多点 Diffif(isArray(newChild)){returnreconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);}// ...}// 处理文本节点if((typeofnewChild==='string'&&newChild!=='')||typeofnewChild==='number'){returnplaceSingleChild(reconcileSingleTextNode(returnFiber,currentFirstChild,''+newChild,lanes,),);}// Remaining cases are all treated as empty.returndeleteRemainingChildren(returnFiber,currentFirstChild);}returnreconcileChildFibers;}
// App.tsximport{useState}from'react'importreactLogofrom'./assets/react.svg'importviteLogofrom'/vite.svg'import'./App.css'functionApp(){const[count,setCount]=useState(0)return(<><div><ahref="https://vitejs.dev"target="_blank"><imgsrc={viteLogo}className="logo"alt="Vite logo"/></a><ahref="https://react.dev"target="_blank"><imgsrc={reactLogo}className="logo react"alt="React logo"/></a></div><h1>Vite + React</h1><divclassName="card"><buttononClick={()=>setCount((count)=>count+1)}>
count is {count}</button><p>
Edit <code>src/App.tsx</code> and save to test HMR
</p></div><pclassName="read-the-docs">
Click on the Vite and React logos to learn more
</p></>)}exportdefaultApp
functioncompleteUnitOfWork(unitOfWork: Fiber): void{// 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.letcompletedWork: Fiber=unitOfWork;do{constcurrent=completedWork.alternate;constreturnFiber=completedWork.return;// 处理当前节点的 completeWork 工作letnext=completeWork(current,completedWork,renderLanes);// 处理当前节点的 completeWork 催生了新 work// (只有 Suspense API 才会走到该逻辑)if(next!==null){workInProgress=next;return;}// 处理兄弟节点的 beginWorkconstsiblingFiber=completedWork.sibling;if(siblingFiber!==null){workInProgress=siblingFiber;return;}// 当前节点及其兄弟节点都完成 completeWork 处理// 以 DFS 的规律回溯处理父节点的 completeWorkcompletedWork=returnFiber;workInProgress=completedWork;// 直到处理到 HostFiber}while(completedWork!==null);// We've reached the root.// 完成 Render 阶段处理if(workInProgressRootExitStatus===RootInProgress){workInProgressRootExitStatus=RootCompleted;}}
下面来看 completeWork 的内部处理
functioncompleteWork(current: Fiber|null,workInProgress: Fiber,renderLanes: Lanes,): Fiber|null{constnewProps=workInProgress.pendingProps;switch(workInProgress.tag){// 省略一堆 tag 处理caseIncompleteFunctionComponent:
caseLazyComponent:
caseSimpleMemoComponent:
caseFunctionComponent:
caseForwardRef:
caseFragment:
caseMode:
caseProfiler:
caseContextConsumer:
caseMemoComponent:
// 冒泡属性bubbleProperties(workInProgress);returnnull;caseClassComponent: {constComponent=workInProgress.type;if(isLegacyContextProvider(Component)){popLegacyContext(workInProgress);}bubbleProperties(workInProgress);returnnull;}caseHostRoot: {// ...bubbleProperties(workInProgress);returnnull;}caseHostComponent: {// ...consttype=workInProgress.type;if(current!==null&&workInProgress.stateNode!=null){// update // 如果 prop 也没变化,则不做任何事,纯复用现成 DOM// 否则 workInProgress.flags |= Update;updateHostComponent(current,workInProgress,type,newProps,renderLanes,);}else{// mountconstrootContainerInstance=getRootHostContainer();// 创建 Fiber 对应的 DOM 节点constinstance=createInstance(type,newProps,rootContainerInstance,currentHostContext,workInProgress,);// 将所有子 fiber 的 DOM 节点挂载到当前 Fiber 刚创建的 DOM 节点下// (体会 DFS 自下而上处理的妙处)appendAllChildren(instance,workInProgress,false,false);// 保存一份引用workInProgress.stateNode=instance;if(// 挂载 props 到创建的 DOM 上finalizeInitialChildren(instance,type,newProps,currentHostContext,)){// 有些 prop 比如自动聚焦不适合在离屏 DOM 里设置,标记 Update 到 commit 阶段挂载时再处理。markUpdate(workInProgress);}}// 冒泡属性bubbleProperties(workInProgress);// ... preloadInstanceAndSuspendIfNeededreturnnull;}}thrownewError(`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in `+'React. Please file an issue.',);}
functionfinishConcurrentRender(root: FiberRoot,exitStatus: RootExitStatus,finishedWork: Fiber,lanes: Lanes,){switch(exitStatus){// ... 省略特殊 case 处理caseRootCompleted: {// The work completed. Ready to commit.commitRoot(root,workInProgressRootRecoverableErrors,workInProgressTransitions,);break;}default: {thrownewError('Unknown root exit status.');}}}
functioncommitRoot(root: FiberRoot,recoverableErrors: null|Array<CapturedValue<mixed>>,transitions: Array<Transition>|null,){do{// 处理上次渲染未来得及执行的所有 useEffect 回调与其他同步任务。flushPassiveEffects();}while(rootWithPendingPassiveEffects!==null);constfinishedWork=root.finishedWork;constlanes=root.finishedLanes;// 解除标志位引用root.finishedWork=null;root.finishedLanes=NoLanes;root.callbackNode=null;root.callbackPriority=NoLane;// Check which lanes no longer have any work scheduled on them, and mark// those as finished.letremainingLanes=mergeLanes(finishedWork.lanes,finishedWork.childLanes);// Make sure to account for lanes that were updated by a concurrent event// during the render phase; don't mark them as finished.constconcurrentlyUpdatedLanes=getConcurrentlyUpdatedLanes();remainingLanes=mergeLanes(remainingLanes,concurrentlyUpdatedLanes);// 从 root.pendingLanes 里去掉本次 Render 阶段已处理完的 lanesmarkRootFinished(root,remainingLanes);// 解除标志位引用workInProgressRoot=null;workInProgress=null;workInProgressRootRenderLanes=NoLanes;// 异步调度 useEffectif((finishedWork.subtreeFlags&PassiveMask)!==NoFlags||(finishedWork.flags&PassiveMask)!==NoFlags){if(!rootDoesHavePassiveEffects){rootDoesHavePassiveEffects=true;pendingPassiveEffectsRemainingLanes=remainingLanes;pendingPassiveTransitions=transitions;scheduleCallback(NormalSchedulerPriority,()=>{// 处理 useEffect 回调flushPassiveEffects();returnnull;});}}// 整颗 Fiber 树是否有待处理的副作用constsubtreeHasEffects=(finishedWork.subtreeFlags&(BeforeMutationMask|MutationMask|LayoutMask|PassiveMask))!==NoFlags;constrootHasEffect=(finishedWork.flags&(BeforeMutationMask|MutationMask|LayoutMask|PassiveMask))!==NoFlags;// 处理副作用if(subtreeHasEffects||rootHasEffect){// ...// 标志 Commit 阶段开始constprevExecutionContext=executionContext;executionContext|=CommitContext;// The first phase a "before mutation" phase. We use this phase to read the// state of the host tree right before we mutate it. This is where// getSnapshotBeforeUpdate is called.// 1. 处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑// 2. 触发 getSnapshotBeforeUpdate 调用的地方commitBeforeMutationEffects(root,finishedWork,);// The next phase is the mutation phase, where we mutate the host tree.// 根据 flags 操作真实 DOM(增删改)// (还会处理 useIntersectionEffect、ref 等)commitMutationEffects(root,finishedWork,lanes);// 切换 current Fiber 树root.current=finishedWork;// The next phase is the layout phase, where we call effects that read// the host tree after it's been mutated. The idiomatic use case for this is// layout, but class component lifecycles also fire here for legacy reasons.// 此时真实 DOM 已被修改,js 可以获取到更新后的 DOM 节点了// 此时主要处理的 effect 就是 useLayoutEffect 、各种 callback 如 setState 的第二个参数的调用commitLayoutEffects(finishedWork,root,lanes);// 标志 Commit 阶段结束executionContext=prevExecutionContext;// ...}else{// No effects.root.current=finishedWork;}constrootDidHavePassiveEffects=rootDoesHavePassiveEffects;// 保存要处理的 passive effect 引用,异步调用时会访问这些引用if(rootDoesHavePassiveEffects){// This commit has passive effects. Stash a reference to them. But don't// schedule a callback until after flushing layout work.rootDoesHavePassiveEffects=false;rootWithPendingPassiveEffects=root;pendingPassiveEffectsLanes=lanes;}// Always call this before exiting `commitRoot`, to ensure that any// additional work on this root is scheduled.// 可能 commit 阶段还会产生一些更新 或者 需启动更低优先级更新的处理,需再次调度 rootensureRootIsScheduled(root,now());// ... // 执行同步任务,这样同步任务不需要等到下次事件循环再执行// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行flushSyncWorkOnAllRoots();returnnull;}
总结来说,commit 阶段主要做的事情如下:
协调 useEffect 的处理
移除本次更新中已处理的 Lanes
根据 effect flags 操作真实 DOM(增删改)以及一些生命周期和 API 的处理
切换 current 树为 wip 树,解除 wip 树引用
调度 root 更新
处理副作用的三个阶段
我们在代码注释里也看到了,React 是通过三个 phase 去处理副作用的,分别是 before mutation、mutation、layout。这三个阶段都会从 HostFiber 开始深度优先遍历地去找对应 effect flag 的 Fiber 节点,自下而上地去处理。 其中 before mutation 阶段做的事情主要就是:
处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑
触发 getSnapshotBeforeUpdate 调用
mutation 阶段做的事情主要是根据 effect flag 操作真实 DOM(增删改),也会处理 useIntersectionEffect、ref 等 API。 layout 阶段做的事情主要就是处理 useLayoutEffect、各种修改 DOM 后的 callback 的调用(如 setState 的第二个参数、render 第三个参数) 这三个阶段源码里处理的情况很多,这里不一一介绍,下面以首次渲染在 mutation 阶段里的处理带大家过一下源码,看看 DOM 是怎么挂载到容器的。
我们可以看到,几乎所有 tag 都要做的两个处理就是 recursivelyTraverseMutationEffects 与 commitReconciliationEffects。 先来看看 recursivelyTraverseMutationEffects 做了啥:
functionrecursivelyTraverseMutationEffects(root: FiberRoot,parentFiber: Fiber,lanes: Lanes,){// Deletions effects can be scheduled on any fiber type. They need to happen// before the children effects hae fired.// 增改前先递归删,免得操作到被删除 DOM 了constdeletions=parentFiber.deletions;if(deletions!==null){for(leti=0;i<deletions.length;i++){constchildToDelete=deletions[i];try{// 删除 DOM 节点commitDeletionEffects(root,parentFiber,childToDelete);}catch(error){captureCommitPhaseError(childToDelete,parentFiber,error);}}}if(parentFiber.subtreeFlags&MutationMask){letchild=parentFiber.child;while(child!==null){// DFScommitMutationEffectsOnFiber(child,root,lanes);child=child.sibling;}}}
直到找到叶子节点或包含 flag 的当前节点后,就会执行 commitReconciliationEffects(当前节点) 继续处理:
functioncommitReconciliationEffects(finishedWork: Fiber){// Placement effects (insertions, reorders) can be scheduled on any fiber// type. They needs to happen after the children effects have fired, but// before the effects on this fiber have fired.constflags=finishedWork.flags;// 我们在创建 wip HostFiber 的 beginWork 时,走的是 reconcileChildFibers 逻辑,// 会给创建的 child fiber 打上 Placement flagif(flags&Placement){try{commitPlacement(finishedWork);}catch(error){captureCommitPhaseError(finishedWork,finishedWork.return,error);}// 去除已处理的 flagfinishedWork.flags&=~Placement;}// ssrif(flags&Hydrating){finishedWork.flags&=~Hydrating;}}
在 commitPlacement 里,我们就会把创建好的离屏 DOM 直接插入到容器下:
functioncommitPlacement(finishedWork: Fiber): void{// 找到当前 Fiber 的父容器 Fiber(容器 Fiber 是指有指针指向真实 DOM 的 Fiber)// 此处是 HostFiberconstparentFiber=getHostParentFiber(finishedWork);switch(parentFiber.tag){caseHostSingleton: {// ...}caseHostComponent: {constparent: Instance=parentFiber.stateNode;if(parentFiber.flags&ContentReset){// 有些标签需要在插入之前清空文本resetTextContent(parent);parentFiber.flags&=~ContentReset;}constbefore=getHostSibling(finishedWork);// We only have the top Fiber that was inserted but we need to recurse down its// children to find all the terminal nodes.insertOrAppendPlacementNode(finishedWork,before,parent);break;}caseHostRoot:
caseHostPortal: {// 容器真实 DOMconstparent: Container=parentFiber.stateNode.containerInfo;// 固定各 renderer 提供 insertBefore 的插入 DOM 方法,web 仅支持 insertBefore API // 所以需求是插入节点的话那么需要找到被插入的兄弟节点// 如果没兄弟节点,就是 append 添加constbefore=getHostSibling(finishedWork);// 执行插入或添加操作insertOrAppendPlacementNodeIntoContainer(finishedWork,before,parent);break;}default:
thrownewError('Invalid host parent fiber. This error is likely caused by a bug '+'in React. Please file an issue.',);}}// 执行插入或添加操作functioninsertOrAppendPlacementNodeIntoContainer(node: Fiber,before: ?Instance,parent: Container,): void{const{tag}=node;constisHost=tag===HostComponent||tag===HostText;if(isHost){conststateNode=node.stateNode;if(before){// renderer API 插入真实 DOMinsertInContainerBefore(parent,stateNode,before);}else{// renderer API 添加真实 DOMappendChildToContainer(parent,stateNode);}}elseif(tag===HostPortal||(supportsSingletons ? tag===HostSingleton : false)){// ...}else{// 如果是非 DOM 节点,就往下找最近的 DOM 节点constchild=node.child;if(child!==null){insertOrAppendPlacementNodeIntoContainer(child,before,parent);letsibling=child.sibling;while(sibling!==null){insertOrAppendPlacementNodeIntoContainer(sibling,before,parent);sibling=sibling.sibling;}}}}
至此,页面上就有渲染好的真实 DOM 了(由于有些宿主标签的限制,会在下一帧才会绘制完整视图,但此时 js 是能拿到该视图对应的真实 DOM 的)。
The text was updated successfully, but these errors were encountered:
我们以首次启动项目到渲染完成为线索,从源码的角度看 React 的内部运行机制,看看 React 是如何实现我们在宏观理解 React 原理里介绍的架构。
只要创建支持并发模式的 React 项目,入口文件都会有类似这么一段代码:
这里就做了两件事情:
createRoot 做的初始化工作
我们已经在宏观理解 React 原理里讲过,Fiber 的工作原理类似于显卡的双缓冲机制,React 项目运行时内存里会存在两棵树:current Fiber 树和 wip Fiber 树,两棵树的协调管理就是通过此处要创建的 root 节点进行的。
我们可以看看 createRoot 的内部实现:
createFiberRoot 内部除了创建 FiberRoot 外,还创建了 current 树的根节点 HostRoot,内部实现如下:
通过此处查看 FiberRoot、Fiber 的数据结构,本质上它们都是 JS 对象,其不同字段代表了不同意义,我们在遇到的时候再说明
![](https://camo.githubusercontent.com/dd7911c2b7def0de5fabfdd7f0e04fbce23c3264418d43c7d099f08a434b0e9a/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f46452d53616468752f6469616772616d2f696d672f3230323430343039323333373539362e706e67)
初始化工作完成后,以树形描述节点如下图
渲染流程
我们可以看到 createRoot 的返回值是 ReactDOMRoot 的实例,然后执行该实例的 render 方法去渲染组件。 我们来看对应的源码:
可以看到执行 render 本质上是执行 updateContainer 方法。
JSX
这个 children 就是 jsx ,举个例子,如下代码:
会被转译成:
看看 jsx 方法的实现:
可以看到,jsx 的执行结果就是 React Element,而 React Element 就是包含了 jsx 标签的类型、属性、key、ref 的对象。
创建更新
接下来看看 updateContainer 源码
可以看到,updateContainer 主要干了这些事情:
调度更新
再来看 ensureRootIsScheduled 是怎么调度的:
时间分片任务入口
Scheduler 调度原理的分析见React Scheduler,我们来看看调度的任务做了哪些事情,也就是 performConcurrentWorkOnRoot 里做了哪些事情:
总结一下 performConcurrentWorkOnRoot 方法做的事情:
Render 阶段
接下来看看 Render 阶段到底处理了哪些事情,以 renderRootConcurrent 的源码来分析:
先来看看是怎样生成一个 WIP 树的:
创建完 WIP 树后,此时以树形描述节点如下图:
![](https://camo.githubusercontent.com/b95c3e3bfccead796cf31911d7fc274758b76a85afc093422263f9ee6b9df46c/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f46452d53616468752f6469616772616d2f696d672f3230323430343039323333393732302e706e67)
接下来执行 workLoopConcurrent 开启一个 loop 持续构建 Fiber 树:
performUnitOfWork 方法做的事情就是根据当前 wip 节点去创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的 Fiber 节点连接起来构成 Fiber 树:
可以看到,核心处理方法就是 beginWork 与 completeUnitOfWork,而且 Fiber 节点的处理顺序是符合 DFS 规律的。
beginWork
这里最关键的是调用 reconcileChildren 生成新的 Fiber 节点并挂在到 WIP.child 下以及把子节点返回出去继续处理。
无论是哪个 tag,几乎都会走这两个步骤,只不过在走这两步前会做一些不同 tag 特殊的逻辑,比如像 HostRoot、HostComponent 就是直接找出要处理的 JSX 对象,像函数式组件 tag 会额外执行 hook 和函数,像类组件 tag 就会实例化执行 render 方法等。
接下来看看 reconcileChildren 具体是怎么创建 Fiber 的
mountChildFibers 和 reconcileChildFibers 其实本质上执行的都是同一个方法,区别在于是否需要跟踪副作用
看下怎么根据 React Element 生成 Fiber 对象的:
总结来说,reconcile children 这个协调过程就是根据 React Element 创建或复用 child fiber 的过程,如果存在 current child 节点,那么可能还会给 child fiber 打上一些 effect flag ,用于 commit 阶段做一些额外的处理比如插入或删除 DOM 节点。
我们上面讲过,workLoopConcurrent 会以 DFS 的方式处理 Fiber 节点,那么直到遇到叶子结点之前都只会进行 beginWork 处理,举以下例子:
beginWork 会处理 Fiber 直到第一次遇到 img 标签,此时 Fiber 树如下图:
![](https://camo.githubusercontent.com/daaf807c4a4f4aab58393fdb3351d48619be050ae819b7e5e66b45ef43e2599b/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f46452d53616468752f6469616772616d2f696d672f3230323430343039323333393939382e706e67)
img 标签是叶子结点,按上述 performUnitOfWork 的逻辑,要对其执行 completeUnitOfWork 方法了。
completeWork
completeUnitOfWork 的作用就是执行当前 Fiber 的 completeWork,协调下一个 Fiber 节点的处理,分几种情况:
当协调处理完所有 Fiber 节点的 completeWork 后,就完成 Render 阶段的处理了,置标志位 workInProgressRootExitStatus 为 RootCompleted。
下面来看 completeWork 的内部处理
与 beginWork 一样,completeWork 会根据 tag 不同做一些不同的处理逻辑,比如对于宿主标签就会初始化对应的 DOM 并串联起子 DOM。此外,所有 tag 也会有一个共性的处理就是 bubbleProperties 属性冒泡,冒泡的是什么属性呢? 所有子 Fiber 的优先级以及副作用处理 effect flag。分别 merge 到 fiber 的 childLanes 和 subtreeFlags 上,好处是之后(commit 阶段)不用遍历整颗 Fiber 树就能知道要处理哪些 lanes 与 effect。
到此为止,Render 阶段的核心代码我们就过完了,接下来就是 performConcurrentWorkOnRoot 里讲过的,要进入 commit 流程了:
Commit 阶段
总结来说,commit 阶段主要做的事情如下:
处理副作用的三个阶段
我们在代码注释里也看到了,React 是通过三个 phase 去处理副作用的,分别是 before mutation、mutation、layout。这三个阶段都会从 HostFiber 开始深度优先遍历地去找对应 effect flag 的 Fiber 节点,自下而上地去处理。
其中 before mutation 阶段做的事情主要就是:
mutation 阶段做的事情主要是根据 effect flag 操作真实 DOM(增删改),也会处理 useIntersectionEffect、ref 等 API。
layout 阶段做的事情主要就是处理 useLayoutEffect、各种修改 DOM 后的 callback 的调用(如 setState 的第二个参数、render 第三个参数)
这三个阶段源码里处理的情况很多,这里不一一介绍,下面以首次渲染在 mutation 阶段里的处理带大家过一下源码,看看 DOM 是怎么挂载到容器的。
首次渲染的 DOM 是怎么挂载到容器的
我们可以看到,几乎所有 tag 都要做的两个处理就是 recursivelyTraverseMutationEffects 与 commitReconciliationEffects。
先来看看 recursivelyTraverseMutationEffects 做了啥:
直到找到叶子节点或包含 flag 的当前节点后,就会执行 commitReconciliationEffects(当前节点) 继续处理:
在 commitPlacement 里,我们就会把创建好的离屏 DOM 直接插入到容器下:
至此,页面上就有渲染好的真实 DOM 了(由于有些宿主标签的限制,会在下一帧才会绘制完整视图,但此时 js 是能拿到该视图对应的真实 DOM 的)。
The text was updated successfully, but these errors were encountered: