看了网上大部分源码材料,今天笔者决定解析React源码,想说一下的是React作为大型前端框架,想要理解内部思想必然是困难的,主要是大部分内容存在耦合,充斥大量关联性代码,
笔者并不觉得自己的文章一定正确,所以希望您和笔者一起撕源码,指出不足
,冲!
React整体架构
- React宏观采取心智模式进行渲染更新,类似加工流程,对每一次任务分配
lane
表示当前任务的优先级,因此对于任务也会根据优先级区别对待,并且任务采取可插队策略,当遇到当前紧急任务时,会退出当前任务循环,进入紧急任务. - 一次任务的调度过程可以以
-
Schedule(安排更新优先级,进行渲染)
-
render(根据当前Fiber树(
current
)渲染新的Fiber树(workInProgress
)) -
commit(进行替换current,依据Fiber树的区别生成或更新dom).
-
Schedule(调度器)
- 作用: 决定当前执行任务的阶段,React细分每次任务调度为一帧(60HZ,16.6ms),为了给渲染线程流出时间更新,js有5ms的执行时间,如果超出那么会打断当前任务.这种方案被称为
时间分片
. - 问题与方案:
-
问题: React为了避免掉帧会为js执行留下5ms的任务调度时间,如果超时,会停止当前任务,将线程执行权交给渲染线程,那么就需要保存这次未完成的更新,并在合适的时机去执行.
-
方案: 因此每次打断当前调度时,都需要创建一个新的任务,该任务在宿主环境存在MessageChannel方法时,会调用该方法,如果不存在,会退而求其次选择setTimeout.
- 1、该创建的任务必须为宏任务,如果是微任务那么依然会阻塞本次任务循环.
- 2、如果采用setTimeout,那么由于浏览器的延迟队列并不会真的精准定时执行,导致一帧下有几毫秒的延迟,所以依然会导致性能损耗.
- 3、rAF,在该次渲染之前检查执行,如果渲染存在延迟,会导致等待,造成性能损耗.
-
React下的代数效应
- React极大的表达了纯函数的概念,将副作用抽离到函数之外,从而保持函数的纯洁性,使输入和输出一一对应,hook和redux就很好的体现了这一点,是用户关注只在函数作用本身.
- Genatator,其实浏览器原生就存在打断任务调度的方式.那不采取的原因是什么呢?
- React团队的sebmarkbage在react的issues提到过这一点,React不使用Genaragtor的原因是在生成器的内部所存在的函数会影响堆栈,并增加语法开销 同时生成器的有状态导致了无法从中间恢复, 例如你顺序执行A、B、C,当你已经执行完B「B内有使用A的值」, 此时B需要重新执行,那么只能重新进入该函数内重新计算A,
- React建立了一套独特的
Fiber架构
,来处理任务中断与恢复、优先级调度等问题.
Fiber架构
- 策略: React在调度阶段采取了
双缓存策略
,解决由于渲染线程和V8线程互斥引起的掉帧问题- 双缓存技术: 为了解释这点,我们以打水为例,当我们在取水时,如果我们只用一个水桶,那我们线性的工作流程必然是缓慢执行的,那么水龙头必然会有停滞的时间(
等待调度
),但是当我们用两只水桶,取水和倒水并发执行时,那么就解决了水龙头停滞的问题,极大的提升了效率,chorme浏览器的html绘制实际也是采取的这种策略,React为了提升重绘效率也是采取了这种方案
.
- 双缓存技术: 为了解释这点,我们以打水为例,当我们在取水时,如果我们只用一个水桶,那我们线性的工作流程必然是缓慢执行的,那么水龙头必然会有停滞的时间(
React.Render
「beginWork」
该过程主要是“递”的过程,在该过程中,我们主要是通过深度优先遍历,对Fiber节点进行标记和重用处理(Diff算法,可以下文),从而创建一颗与current Fiber关联的workInprogress Fiber树.
- 笔者偷懒一下了? | 这个过程最重要的步骤就是『diff算法』和“递”的过程.目的就是为completeWork做铺垫.
- 对于classComponent,会执行updateClassComponent,在本函数中我们会执行
updateClassInstance
系列函数(需要深入),在该函数中,会调度processUpdate,它会解开本次渲染中的通过setState将本次更新放入到Fiber.updateQueue.sharing.pending形成循环链表,和上次更新的lastUpdate形成单链表(Fiber.effects),从而顺序执行获取newState
「completeWork」
进入当前阶段, 我们对Fiber的处理已经完成,接下来就是进行Dom的处理.
在这里笔者暂时以HostComponent作为Tag类型进行解析
.
该阶段的处理主要分为
更新
和创建
两个阶段.笼统点说该阶段就是对对应Fiber的属性进行挂载或更新(尤其是props).
主函数入口
-
// 当前workinprogress Fiber对应tag为HostComponent case HostComponent: { // 从栈中弹出当前Fiber popHostContext(workInProgress); // 拿到根节点实例 const rootContainerInstance = getRootHostContainer(); // type对于Function Com表示函数本身 // 对于Class Com表示类本身 // 对于原生标记标是标签名 const type = workInProgress.type; // 已有Fiber树存在, 更新阶段 if (current !== null && workInProgress.stateNode != null) { // 更新props updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); //标记ref Flags, 表示该Fiber ref需要更新. if (current.ref !== workInProgress.ref) { markRef(workInProgress); } } else { // 新建Component 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; } // 获取当前游标指向的Fiber, 指向当前Fiber的nameSpace const currentHostContext = getHostContext(); // 是否被强制更新, 我们不过多分析 const wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { if ( prepareToHydrateHostInstance( workInProgress, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); } } else { // 新建instance // 创建DOM实例 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 将当前实例和所有子节点建立dom或return的绑定 appendAllChildren(instance, workInProgress, false, false); // 当前Fiber指向当前instanceDOM workInProgress.stateNode = instance; if ( // 设置props(初始化属性),并根据是否是autoFocus标记是否update finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); } } if (workInProgress.ref !== null) { markRef(workInProgress); } } // TODO: 优先级设定(我们不多讨论) bubbleProperties(workInProgress); return null; 复制代码
}
创建阶段.
这里的内容和Diff是相关联的,实际进入这里的操作是由于diff中,我们的节点无法复用使得我们必须新建Fiber「stateNode为null」,然后在这里我们再新建dom,并和workInprogress.stateNode以及子节点的domTree连接
- appendAllChildren(instance, workInProgress, false, false);
- 该函数作用是建立当前实例和子节点的关联.并且很好的表达了
归
的含义,希望您细品 -
appendAllChildren = function( parent: Instance, workInProgress: Fiber, needsVisibilityToggle: boolean, isHidden: boolean, ) { // 对于Fiber对应的dom元素, 进行元素挂载. // 对于子Fiber, 进行自当前Fiber以下的所有childFiber的return的“归”式向上绑定.向上连接 let node = workInProgress.child; // 该函数循环旨在对于不同类型的Fiber,我们需要有不同的处理,如果当前Fiber并不对应原生Component或者Text,那么它并不能建立DOM之间的关系(ClassComponent等),我们需要深度优先遍历进入当前Fiber.child,找到挂载关系.并不断建立return指向「归」,当然我们还需要处理sibling的关系,那么所有的子Fiber就和当前的实例建立了appendChild. // ***本次循环过后, 当前Fiber对应的instance与child层的instance建立了父子关联*** while (node !== null) { // 符合DOM挂载条件. if (node.tag === HostComponent || node.tag === HostText) { // 父子元素挂载 appendInitialChild(parent, node.stateNode); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child !== null) { // 这里我们继续深入child,并且continue node.child.return = node; node = node.child; continue; } // 归的过程已经当顶, return if (node === workInProgress) { return; } // 当前node已对应原生DOM, 深度优先结束,我们回溯查找. while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } // sibling存在,我们让sibling和node.return建立父子关系 node.sibling.return = node.return; node = node.sibling; } }; 复制代码
- 该函数作用是建立当前实例和子节点的关联.并且很好的表达了
- finalizeInitialChildren
- 该函数看似不起眼,实际是completeWork的重要过程,感受一下痛苦吧
-
export function finalizeInitialChildren( domElement: Instance, type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, ): boolean { // 设置属性(重点关注函数) setInitialProperties(domElement, type, props, rootContainerInstance); // 查找props的autoFocus决定是否标记update Flags.(不深究) return shouldAutoFocusHostComponent(type, props); } 复制代码
- setInitialProperties函数主要是将我们新的pendingProps作用到DOM上.
-
export function setInitialProperties( domElement: Element, tag: string, rawProps: Object, rootContainerElement: Element | Document, ): void { // 我们根据标签的“-”以及props的is(动态绑定)决定是否是自定义组件 const isCustomComponentTag = isCustomComponent(tag, rawProps); let props: Object; // 这个过程中我们会对部分原生组件所含有的独特Attr并没有采用RootContainer去监听,而是原始的放在了dom上.「参照React事件篇.」 swich(tag) { case 'dialog': // 事件注册 listenToNonDelegatedEvent('cancel', domElement); listenToNonDelegatedEvent('close', domElement); props = rawProps; break; ...... } // 判断props是否合法, style、children和dangerous、单标签的children,不合法throw Error assertValidProps(tag, props); // 设置初始化属性. // 该过程我们会查找style列表,将它标准化,通过dom.style设置到dom上,并设置children和dangerousHTML. setInitialDOMProperties( tag, domElement, rootContainerElement, props, isCustomComponentTag, ); // 这个过程, 我们主要是对拥有defaultValue,value,defaultChecked等属性标准化,并且将defaultValue的值赋值给node.value,具体可以参照源码. swich(tag) ... } 复制代码
更新阶段.
- updateHostComponent.更新主函数.
-
updateHostComponent = function( current: Fiber, workInProgress: Fiber, type: Type, newProps: Props, rootContainerInstance: Container, ) { const oldProps = current.memoizedProps; if (oldProps === newProps) { // 新旧props一致直接return return; } const instance: Instance = workInProgress.stateNode; const currentHostContext = getHostContext(); // 准备更新「重点函数」 const updatePayload = prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, currentHostContext, ); // 需要更新的props workInProgress.updateQueue = (updatePayload: any); if (updatePayload) { // 为当前Fiber标记需要更新 markUpdate(workInProgress); } }; 复制代码
- prepareUpdate函数会直接进入diffProperties,对比Props的不同返回更新数组.下面我们进入主函数.
- 我们进入关键点.
- 在diff中我们分为
- 1、对于可受控组件,我们初始化它们的defaultValue,defaultChecked,value,checked值
- 2、检测props合法性.
- 3、比较两个props的不同之处.(这里React对style的处理是,将newProps上没有,但old上有的,设置为”,newProps上diff出来的设置为key-value).
经过beginWork和completeWork的分析,笔者认为React采取深度优先遍历的目的是:“递”阶段,我们需要diffChildFiber,找到可复用的节点提升性能,而这是“归阶段做不到的”,“归”阶段,我们又需要对于建立Dom之间的成功连接,这一操作被放在归阶段又是十分高效的(新节点不断连接新子树的过程)
「commitRoot」
commit阶段主要负责props对Fiber-Dom的属性设置以及副作用事件和生命周期函数的执行.
调度概要
- 在commit阶段我们会进行下面几个过程,我们逐渐深入
- 1、清空进入commit之前的副作用回调
- 2、创建在本次commit中的副作用调度函数,(在本次commit之后执行, 下一个宏任务),在该阶段中,我们会经过「标记执行上下文CommitFlag」,「深度优先“归”阶段执行卸载所有副作用函数」,「深度优先执行所有副作用函数」
注意是执行完所有卸载函数后再执行主函数
,并且这里采用了树的思想,会根据subTree的flags来确定在当前节点的子树下是否还有副作用,如果没有则跳到sibling或者return(这里我们打个tag,后面来仔细分析)
- 3、
commitBeforeMutationEffects
, 对于上一次渲染时被标记无法复用的Fiber,判断beforeBlur,存在执行,对于classComponent,执行getSnapshotBeforeUpdate
生命周期等一系列操作
- 4、
commitMutationEffects
「“归”调度,从子节点向父节点的触发顺序」.- 1、执行标记删除的Fiber对应dom的移除,以及自它而下的Fiber的unmounted回调.
- 2、执行Fiber上flags的对应处理
- 对于FunctionComponent会调度useInsertionEffect(返回函数和函数体,V18bata新增), useLayoutEffect「卸载」函数
- 对于HostComponent会首先进行props的变化更新
- 3、对于Ref标记进行commitDetachRef,删除指向
- 5、
commitLayoutEffects
「“归”过程调度,从子节点向父节点发散」- 1、对于FunctionComponent会调度useLayoutEffect主函数,
这里记一下,在执行时属于render的同步,会阻塞浏览器,下面讲到「useEffect」我们再比对一下
, - 2、对于ClassComonent.
- 通过fiber.alternate(上次渲染的fiber) === null来判断是否为首次渲染,从而执行
componentDidMounted
或componentDidUpdate
- 同时会执行在component.updateQueue上的Effects链表的回调callback
- 通过fiber.alternate(上次渲染的fiber) === null来判断是否为首次渲染,从而执行
- 3、我们在本次操作中,会使用commitAttachRef,对Ref进行赋值
- 1、对于FunctionComponent会调度useLayoutEffect主函数,
上述三个重要过程都采用深度优先遍历的方式,“递”“归”化执行函数,这也是Fiber架构中的一种重要算法.
- 笔者在在翻到这里时,却发现本次更新的useEffect是没有执行到的.通过对源码的调试发现才发现了是被作为了另一个宏任务, 这也是为什么我们说useEffect是异步调度的,因此会在useLayoutEffect之后执行
源码分析
commit之前:
ensureRootIsScheduled
(确定任务调度) ->performConcurrentWorkOnRoot
(开启并发调度)->renderRootConcurrent
(开启beginWork-completeWork,返回出口状态)finishConcurrentRender
(开启commitRoot, commit阶段开始)- 在下文中我们大致讲解了开启commitRoot的过程,「下文不针对无源码基础读者,希望您看下文时,先了解源码,希望谅解」
-
function workLoopConcurrent() { // 生成缓存树 // Perform work until Scheduler asks us to yield // 当workInProgress表示更新完成 while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } } 复制代码
-
// 该函数在completeUnitOfWork(Complete阶段)执行 // 标记当前FiberTree已complete完成 if (workInProgressRootExitStatus === RootIncomplete) { workInProgressRootExitStatus = RootCompleted; } 复制代码
-
// 该返回为renderRootConcurrent的状态返回,用来标识Fiber树状态 return workInProgressRootExitStatus; 复制代码
-
//finishConcurrentRender // switch (exitStatus) { // 正式进入commitRoot,开始commit case RootCompleted: { // The work completed. Ready to commit. commitRoot(root); break; } } 复制代码
commit开始
下文代码片段取自笔者认为阶段重要代码片段,并不完整.
- commit -> commitRootImpl(commit在该阶段分配了以
离散事件优先级
为参数的更新优先级
,并执行Impl调度) 清空上一次中断未完成的副作用回调
-
do { flushPassiveEffects(); } while (rootWithPendingPassiveEffects !== null); // 清空上一次存在的副作用回调 复制代码
-
创建下一个宏任务以「普通事件优先级」调度执行副作用函数
-
if ( // 判断副作用,存在则调度 (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags ) { // ??? (2022.2.14,9.23 一个人的情人节~) if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; pendingPassiveEffectsRemainingLanes = remainingLanes; // 创建调度任务 scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null; }); } } 复制代码
- 在flushPassiveEffectsImpl函数中调度销毁副作用函数和副作用主函数,都是通过“递”“归”的方式执行
-
// flushPassiveEffectsImpl // // 执行销毁函数 commitPassiveUnmountEffects(root.current); // 执行挂载函数 commitPassiveMountEffects(root, root.current); 复制代码
“递”阶段,我们通过subtreeFlags(子数是否存在副作用)来确定是否有必要继续深度优先.(后面阶段也都都是这种算法)
-
// commitPassiveUnmountEffects_begin // 如果不存在那么,直接进入归阶段,优化性能 if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) { ensureCorrectReturnPointer(child, fiber); nextEffect = child; } else { commitPassiveUnmountEffects_complete(); } 复制代码
-
// commitPassiveUnmountOnFiber (在“归”阶段执行,由子到父) mount同理 function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { switch(finishedWork.tag) { case FunctionComponent: ... { if(...) // 任务中断 {} else { commitHookEffectListUnmount( HookPassive | HookHasEffect, // 这个flags监控updateQueue.effects. // 如果effects为副作用函数则执行它的destory finishedWork, finishedWork.return, ); } } } } 复制代码
- 该阶段我们不过多研究Hook相关内容,在后面我们会再开一篇
-
commitBeforeMutationEffects
- 本阶段我们并有进行关键性操作,笔者只做部分总结
- 1、对于deletionFiber触发beforeblur事件(“递”阶段)
- 2、对于classComponent,触发getSnapshotBeforeUpdate生命周期(“归”阶段)
commitMutationEffects
- 该阶段作为commit核心部分,我们重点讲解.
- 1、还是与上文一致,我们依然采用深度优先遍历+subTree状态标记来处理副作用.
- 2、改阶段我们会对于diff过程中无法复用的FiberDom(deletion)进行节点移除.
- commitMutationEffects_begin
const deletions = fiber.deletions; if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; try { // 移除当前Fiber和对应的dom,以及自它以下的Fiber的unmounted回调 commitDeletion(root, childToDelete, fiber); } catch (error) { reportUncaughtErrorInDEV(error); captureCommitPhaseError(childToDelete, fiber, error); } } } 复制代码
- 在commitDeletion内我们移除当前Fiber并且断开当前Fiber和其return的连接
- commitDeletion主函数
unmountHostComponents
(移除Fiber)- 该函数也是React的关键点,可能您在未读源码之前和笔者一样认为移除一个Fiber是再简单不过的事,这里希望您带着以下几个问题深入学习.
1、Fiber对应dom移除是如何进行的(Fiber可能对应一个组件,那也就意味着它的父元素要删除组件内的所有元素)?
2、如果是HostRootFiber那么怎么去从父元素上移除它呢,或者说如果拿到其元素呢
-
首先,对于当前Fiber而言,我们要移除它对应的相应dom,那首先就是找到它的ParentNode.
- 可以看到这个findParent是在循环内部的,原因是对于
cratePortal
而言,它的挂载点是可以不为dom结构的父元素的,所以当我们遍历该Fiber时,如果该Fiber为Fragment,那么它内部如果存在portalFiber,我们就会重定义currentParent,然后进入循环,但是当我们从portal出来后,它的currentParent依旧为protalFiber的parent,而如果此时portalFiber.sibling不为null,我们就需要去重新定义currentParent,所以将它放在循环内部.
-
while (true) { if (!currentParentIsValid) { // 该标记是否找到父dom let parent = node.return; findParent: while (true) { const parentStateNode = parent.stateNode; switch (parent.tag) { case HostComponent: currentParent = parentStateNode; currentParentIsContainer = false; break findParent; case HostRoot: // root根实例 currentParent = parentStateNode.containerInfo; currentParentIsContainer = true; break findParent; case HostPortal: currentParent = parentStateNode.containerInfo; currentParentIsContainer = true; break findParent; } parent = parent.return; } currentParentIsValid = true; } 复制代码
- 可以看到这个findParent是在循环内部的,原因是对于
- 在这之后,我们就会去判断当前Fiber的类型,对于HostComponent或者HostText(原生节点)我们就会
卸载该Fiber节点,并DFS该Fiber.child执行其相应的回调,并通过currentParent Remove该FiberDom
. -
if (node.tag === HostComponent || node.tag === HostText) { // 执行当前Fiber以下的所有unMounted commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor); // 判断父元素是否为容器,父dom杀出元素 if (currentParentIsContainer) { removeChildFromContainer( ((currentParent: any): Container), (node.stateNode: Instance | TextInstance), ); } else { removeChild( ((currentParent: any): Instance), (node.stateNode: Instance | TextInstance), ); } 复制代码
- 这里HostPortal又一次被设置了,笔者认为这也是该函数的难点处
-
if (node.tag === HostPortal) { if (node.child !== null) { currentParent = node.stateNode.containerInfo; currentParentIsContainer = true; node.child.return = node; node = node.child; continue; } } 复制代码
笔者认为
: PortalFiber是DFS中一个特殊分支,在findParent中我们是针对该Fiber“树”
下的节点的(一般被确定),而在此处,当遇到HostPortal时,它像是被作为一颗新的树跳出当前结构执行(需要重定义).- 在此处,我们发现当对于ReactComponent时(这里以此为例),我们并不把它当回事?, 因为它与dom没有关系,我们只执行它的副作用,然后查找该Fiber的对应dom结构进行remove.
-
else { // ReactComponent情况下 // 执行自当前Fiber以下的unmounted commitUnmount(finishedRoot, node, nearestMountedAncestor); // 此时,当前继续循环当前Fiber.child. // 直到找到tag === dom的,从而在循环内删除element if (node.child !== null) { node.child.return = node; node = node.child; continue; } } 复制代码
-
// 单节点直接结束. if (node === current) { return; } // 当前层的Fiber已删除,返回上一层(不用考虑被删除的子层,会随父Fiber一起remove) while (node.sibling === null) { // “归”过程 if (node.return === null || node.return === current) { return; } node = node.return; if (node.tag === HostPortal) { // 我们所说的,parent需要被重定义,也只有这里需要被重定义 currentParentIsValid = false; } } // 这里结合findParent看,我们需要知道的是,我们只会进入parent下的第一层子节点 // 因为在前面判断一旦遇到hostComponent,就直接删除它 node.sibling.return = node.return; node = node.sibling; 复制代码
- 笔者花了大量的篇幅去解释该函数,原因是该函数松耦合,并且React源码大量采用这种DFS,而该函数也很好的体现了Fiber和Dom之间的关系,希望对您有帮助.
commitMutationEffectsOnFiber(commitMutationEffects_complete“归”阶段主函数)
在该阶段React处理了Fiber的flags
(节点变化)- 1、在该阶段对于标记了Ref的会卸载Ref绑定.
- 2、对于PlaceMent(插入节点)会执行commitPlaceMent.
- 1、获取HostComponentFiber(Portal,HostRoot),最近的祖先节点
- 2、获取要被插入位置的下一个节点,如果存在调用insertBefore,否则调用appendCHild.
getHostSibling(finishedWork)
「666」- 该函数可不简单,
首先自该节点开始向上查找(包括该节点)祖先Fiber的sibling,如果祖先Fiber为“原生节点”,但是sibling依然为null,那么说明没有兄弟节点
如果sibling存在,我们需要判断它是否代表“原生节点”,并且还要判断是否被标记PlaceMent,如果被标记了说明该节点为插入节点,也就意味着它还没用被插入dom中,我们需要继续向后查找,除此之外还要判断HostPortal,它不存在父Fiber下,需要继续向后查找
- “循环标准: 自底向上,遇sibling,进入sibling,父层遇“原生节点”退出,sibling为null …. ”
- 希望读者可以阅读该函数源码,也是非常有递归思想,并且细节很多.
- 该函数可不简单,
- 3、
insertOrAppendPlacementNode
- 接下来我们需要插入该Fiber,该过程中我们判断当前PlaceMentFiber的tag,如果为“原生节点”,结合siblingFiber,直接插入, 否则
递归当前Fiber.child,将该Fiber下的所有子dom全部插入, 仔细体会! ! !
- 接下来我们需要插入该Fiber,该过程中我们判断当前PlaceMentFiber的tag,如果为“原生节点”,结合siblingFiber,直接插入, 否则
commitWork(处理更新副作用)
- 该阶段主要是处理了Fiber更新引起的副作用回调触发,以及props更新变化.
- 我们基于FunctionComponent和HostComponent展开分析.
- 对于FunctionComponent等,我们可以看到执行了
commitHookEffectListUnmount
和commitHookEffectListMount
两个函数. - 可以看到首先执行了第一个参数均为HookInsertion | HookHasEffect的UnMount和mount(注意执行顺序).然后顺序执行了LayeOut的Ummount.
- 那么顺序就是
InsertionUnmount
->InsertionMount
->layOutUnmount
-
switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: { commitHookEffectListUnmount( HookInsertion | HookHasEffect, finishedWork, finishedWork.return, ); commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); commitHookEffectListUnmount( HookLayout | HookHasEffect, finishedWork, finishedWork.return, ); 复制代码
- 下面我们进入Unmount函数去看一下
-
// 执行副作用函数的return函数(destory),umMount function commitHookEffectListUnmount( flags: HookFlags, finishedWork: Fiber, nearestMountedAncestor: Fiber | null, ) { // 所有副作用更新队列 const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & flags) === flags) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { // 执行销毁函数 safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); } effect = effect.next; } while (effect !== firstEffect); } } 复制代码
- 对于FunctionComponent等,我们可以看到执行了
- 这里我们还没有对Hook展开分析,但是如果您坚持到了这里,想必对Hook一定有所了解.
- 首先对每个Fiber而言, 它的updateQueue是
环形链表
,优势是一个变量就可以实现“获取firstEffect” , “获取LastEffect”, “O(1)实现add Effect”. - 对于FunctionComponent,它的updateQueue就是存储每个副作用函数(effect),具体而言就是useEffect(主函数和return 函数「卸载函数」),useLayOutEffect(…),以及Reactv18新增的useInsertionEffect(…).
- 因此每个effect上都tag,标记该effect类型,到这里你应该想明白了上面执行的
commitHookEffectListUnmount
为什么第一个参数不一样,其实就是遍历updateQueue并执行第一个参数类型的effect,执行它的destory(主函数内的return函数),那么同理commitHookEffectListMount就是执行effect的主函数
- 对于HostComponent.
- 会调度
commitUpdate
,将complete阶段拿到的newProps更新到原生dom上(style,children,dangerouslySetInnerHTML),其余的属性会通过(for in)查找属性,如果在原生节点中存在的属性就允许直接赋值.其中还有对自定义组件的处理,这里不多赘述 - 这里笔者着重提一个重点,就是value和defaultValue, 想必
受控组件和非受控组件
应该是您所熟知的,但是如果追溯原理,那就直接体现了您的思考深度,(以input为例) defaultValue并没有什么特殊,不过是原生节点的一个属性,当为一个组件设置value时,组件受控,不随“默认原生”输入事件变化,实际上input并不是没有变化,只是被重置了而已,在scheduleEvent阶段,当case ‘input’时,会调用ReactDOMInputUpdateWrapper
,在该函数中,会执行value和defaultValue的设置,并且会比较node.value(输入完成后的值)和props.value一致性,如果不一致,props.value会覆盖node.value,所以导致输入失效.而该组件也就是著名的受控组件
(checked同理) - 上面内容本应该在事件机制中阐述,但是笔者发现,在该阶段的updateProperties也已经存在该内容,所以就在本篇阐述了,希望您能结合下文的事件机制深入体会.
- 会调度
- commitLayOutEffects
- 此时FinishedWork已经全部挂载完成,开始执行mount(update)回调
commitLayoutEffectOnFiber
(“归”阶段,由子到父)-
// 该LayoutMask === Update | Callback | Ref | Visibility(标记是否有更新); if ((finishedWork.flags & LayoutMask) !== NoFlags) {...} 复制代码
- 在该函数的结尾处(
这里埋个伏笔
),我们会判断Ref,并设置ref引用,顺便说一句ref为对象或者函数的目的就是实现数据的正确引用. -
if (finishedWork.flags & Ref) { commitAttachRef(finishedWork); } 复制代码
- 这里我又用到了
commitHookEffectListMount
,它的flags = HookLayout | HookHasEffect. - 现在我们知道了我们的执行顺序为
InsertionUnmount
->InsertionMount
->layOutUnmount
->layOutUnmount
. - 结合上文我们知道
该函数末尾会对标记Ref的对象设置该Ref的引用
.并且我们Ref都会用来标记子Dom
,并且函数和其他过程一样为子到父的执行顺序
,这也就以为这当我们还在执行到父Fiber的useLayOutEffect时,它所设置的ref已经被设置了指针指向. - 综上,我们可以得出,在layOutEffect执行时,
我们已经设置了Ref指向
这也是它区别于insertionEffect的地方 -
switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); } 复制代码
- 对于classComponent
- 会判断current(上一次更新的Fiber)是否存在来决定调用componentDidMount或componentDidUpdate.
- 然后会调用
commitUpdateQueue
对所有setState的回调函数进行遍历执行
-
- 到此为止,当前
“宏任务”
下的commit函数系列已经完成,是的,还记得我们之前的说的flushPassiveEffects
函数吗,该函数会执行副作用调度(useEffect),同样的,它也是首先执行完所有effect.destory,然后执行create.因此我们说的useEffect实际上是独立于当前宏任务执行的,这也是为什么我们有时在useEfect中设置样式时,会出现闪屏,如果要解决该问题,应该使用useLayOutEffect,在LayOutEffect中,如果setState,会生成一个新的syncCallback,在flushSyncCallbacks中添加到执行队列,同步进行再一次更新(后面会深入分析)
好了,我们的commit阶段解读结束了,这一部分是笔者撕的较为细的,因为重要操作较多,并且实际开发中也常经过这些过程.开冲hook,?
ReactHook篇
en~~,笔者在写到这里时,有点乏了,打工人真的很辛苦,好吧,我们回到正题,首先在看到这一篇的时候希望您最起码对整个渲染过程有有大体的认识了,hook作为现阶段react的主要开发模式,我想了解它底层的原理是很重要的(主要为了吹牛),笔者在撕源码之前,曾看过卡颂大佬的文章,笔者认为卡颂大佬的文章可以作为启蒙篇,对于部分内容如果不深入还是有疑惑的.我们开冲了!
Hook基础篇
-
首先:
您知道Hook执行的阶段吗?
您知道hook为什么不允许卸载嵌套语句中吗?
-
回答第一个问题: 在beginWork阶段,如果您认真阅读了React,那么您一定知道该阶段是在生成FiberTree的,那么这都是依赖于拿到renderFiber的.而对于FunctionComponent,就必须执行component().
-
第二个问题我们后面解释.
-
renderWithHook(FuncionCompnent主函数)
-
nextChildren = renderWithHooks( // 执行当前Function current, workInProgress, Component, nextProps, context, renderLanes, ); 复制代码
-
可以看到对于FunctionComponent而言,执行Function阶段是从父到子的,但是如果您有仔细看
commit
阶段的内容,就能知道执行Function内部的effect却是在commit的,所以现在我们知道对于Hook而言,执行该函数值只是在为commit做准备
-
React对于mount和update有两套hook
-
// 全局调度器列表 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // 首次挂载时hook const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, .... } // 更新时hook const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useInsertionEffect: updateInsertionEffect, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, ... } 复制代码
-
下面我们来讲一下Hook在Function中的存在方式,Hook会以
对象节点
的形式存在Fiber.memoizedState中.-
const hook: Hook = { // 存储的值 memoizedState: null, baseState: null, // 上次更新的队列 baseQueue: null, // hook更新队列(循环链表 便于插入和找到头部) queue: null, // 与下一个hook形成单链表 next: null, }; if (workInProgressHook === null) { currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; } 复制代码
-
-
这里不可以将hook.memoizedState和fiber.memoizedState弄混,前者表示
当前hook
的相关信息(输出结果,依赖等),后者表示当前Fiber
下存在的hook单链表,它们是毫不相干的. -
这里笔者顺带提一下updateWorkInProgressHook,该函数主要是复用currentFiber的hook,因为
我们在cloneFiber之后就丢失了hook链表,而在updateHook时,我们的currentFiber上是存在相应的Hook的所以我们就可以通过alternate拿到current.memoizedState(上次更新的Hook链表),之后直接clone oldHook
,这时我们上面的第二个答案就出来了,之所以不允许hook写在嵌套,是因为每次执行component时,拿到clone的oldHook是通过hook.next获取的,如果由于嵌套的原因,使本次hook链表长度和mount时不一致,就会使clone出错 -
我们再来看一下hook中的属性.
- memoizedState(存储当前hook的重要信息,依赖等)
- baseState 表示基于该数据进行更新
- baseQueue 上次被中断的更新队列
- queue 存储更新队列pending,dispatch,lane等信息.
-
Hook全面解析
useCallback
- mountCallback
-
const nextDeps = deps === undefined ? null : deps; // 保存hook状态 hook.memoizedState = [callback, nextDeps]; return callback; 复制代码
-
- updateCallback
-
const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 参照上面mount的数据 const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { // 上一次的依赖 const prevDeps: Array<mixed> | null = prevState[1]; // 比较依赖变化,如果为未变化,直接复用,(Object.is) if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } // 创建 hook.memoizedState = [callback, nextDeps]; return callback; 复制代码
- 注: 对于useCallback判断依赖的原因是每一次的函数执行的作用域是不同的,这意味着如果依赖变化,但是函数未重定义,就会使数据指向不符合预期,所以React在这里既保证了效率有保证了函数的正确性.
-
useEffect
- mountEffect
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
// 主函数
create,
deps,
);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 表示当前Fiber存在副作用回调,为commit阶段判断是否需要深入做准备
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
复制代码
- 这里关注一下上面的pushEffect,这步操作
不仅更新了hook,而且更新了updateQueue(副作用循环链表)
.
function pushEffect(tag, create, destroy, deps) {
// 生成effect节点
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue =(currentlyRenderingFiber.updateQueue);
if (componentUpdateQueue === null) {
// 创建副作用循环链表, 对象只存在lastEffect用于指向最后一个副作用
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// 循环链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 更新循环链表
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
复制代码
- updateEffect
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 这时候destoy必然已经在上一次commit阶段执行
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 缺少HookHasEffect字段,表示当前hook依赖不变,commit时不必执行
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 表示当前fiber需要执行当前副作用函数.
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
复制代码
- 其实updateEffect和uodateCallback并无大的差异,但还是有几点巧妙的地方,我们来看一下.
- 1、
destroy = prevEffect.destroy;
这里可以看到我们对销毁函数进行了赋值,这是因为如果currentFiber存在,那么在mount时必然已经执行过该副作用函数,在执行结束我们会将返回值存在该hook.destory中,在这里我们就可以直接复用它,在commit阶段的unmountEffect函数中执行. - 2、HookHasEffect,可以看到的是该flags对于updateQueue而言,是mount和update的区别,如果您仔细看过笔者
commit篇
的文章,就可以发现React在执行副作用函数时是用flags来遍历updateQueue,只有flags全等才会得到执行,而该标记就是决定当前Fiber是否因依赖变化而需要执行回调的flags.- 那么这时您可能会有疑问了,那为什么在依赖不变时,依然需要pushEffect进入updateQueue,这么做是因为在组件销毁时,需要调用所有effect的destory函数,所以即使依赖不变,也需要pushEffect
- 1、
useLayoutEffect和useInsertionEffect和useEffect基本一致,只有flags和Fiberflags有区别(标记函数执行时机),本文不多做赘述.
useImperativeHandle
- 简析: 子组件使用useImperativeHandle可以实现子向父传参,该函数往往与forwardRef共用
- mountImperativeHandle
function mountImperativeHandle<T>(
ref,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
let fiberFlags: Flags = UpdateEffect;
// 与layout一致的执行优先级
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
function imperativeHandleEffect<T>(
create: () => T,
ref,
) {
// 设置ref指向
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
// 返回销毁函数
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}
复制代码
- 其实这里可看出它的执行时机和方式和useLayOut基本一致,这里笔者顺带提一嘴,我们看到它的更新标记是updateEffect,再看useEffect是Passive,如果您仔细阅读了commit阶段,就可以知道,update标记是在当前宏任务下判断执行的(
commitWork
),而Passive会被创建新的任务调度在下一次执行.那么这里使用layOut的优先级也是可以理解的,因为我们认为在useEffect中是已经标记完ref指向的. - 那么其实该函数和在useLayOut中设置ref.current一致,但这不是为了React16后从命令式编程到声明式编程的一小步嘛
updateImperativeHandle依然是比对依赖决定ref指向是否变化,希望您自行翻译.
useMemo
基本与useCallback一致,只有hook.memoizedState保存的值又变量变为了方法.
useReducer(重点,代表性hook)
useReducer是hook中极具代表性(这个真的可以吹牛?)
- useReducer实际是redux的纯函数思想的复用,都是Dan大神的杰作,对于判断处理多个子值的复杂入参极其友好.这里笔者并不多做介绍,我们直接从源码角度来看React的实现.
- mountReducer
-
function mountReducer<S, I, A>( reducer: (S, A) => S, // reducer, 纯函数 initialArg: I, // 默认值 init?: I => S, // 以初始值为参数,将返回作为初始值 ): [S, Dispatch<A>] { const hook = mountWorkInProgressHook(); // 默认值 let initialState; if (init !== undefined) { initialState = init(initialArg); } else { initialState = ((initialArg: any): S); } hook.memoizedState = hook.baseState = initialState; // 更新队列 const queue: UpdateQueue<S, A> = { pending: null, interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), }; hook.queue = queue; const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind( null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch]; } // useReducer的第二个出参 function dispatchReducerAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) { const lane = requestUpdateLane(fiber); // 本次更新, const update: Update<S, A> = { lane, action, hasEagerState: false, eagerState: null, next: (null: any), }; // 判断render是触发Reducer if (isRenderPhaseUpdate(fiber)) { enqueueRenderPhaseUpdate(queue, update); } else { // 在事件或者其他宏任务中触发. 执行任务调度 enqueueUpdate(fiber, queue, update, lane); const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { entangleTransitionUpdate(root, queue, lane); } } markUpdateInDevTools(fiber, lane, action); } 复制代码
- 看了代码也许您有点晕,甚至无法理解,下面笔者来解析一下上面的操作.
- 1、第三个参数init会作为默认rducer在初始化时将initialArg传入该函数,再将返回值作为默认值
- 2、对于
依据入参获取出参的纯函数hook
(useState类似「后面讲」),React维护了hook.queue,以提升性能.-
可以看到在
dispatch
中我们执行了enqueueUpdate,这其实就是在将本次更新作为节点放入queue.pending中(循环链表形式存在),(具体分析我们会在update时介绍). -
也许你注意到了isRenderPhaseUpdate函数,这其实是在判断当前dispatch是否是在
当前render阶段
,试想如果当前dispatch是在当前render阶段,那么我们不断的scaduleUpdate,就会进入死循环.React可不笨,我们在enqueueRenderPhaseUpdate()
时会设置didScheduleRenderPhaseUpdateDuringThisPass = true
.- 看下面的代码就能知道,React会继续循环执行Component,如果每次执行都有setReduce,那么就直接抛出错误.
-
let children = Component(props, secondArg); // 判断render阶段的update(dispatch操作等) ? // 对于在render阶段进行的dispatch会循环执行 // Check if there was a render phase update // if (didScheduleRenderPhaseUpdateDuringThisPass) { // 记录重绘次数 let numberOfReRenders: number = 0; do { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; if (numberOfReRenders >= RE_RENDER_LIMIT) throw Error(...) } .... children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); 复制代码
-
- 好了,现在我们经过了mount阶段,关键点:
updateDispatch一直在pending中
-
- rerenderReducer
- 该函数会在render时执行dispatch而发生的重新执行component(),作为useReducer执行.
- 该函数会遍历由dispatch引起的update,
直接
循环执行,并重新赋值newState. - 具体源码,希望读者自己分析
- updateReducer
- 该函数会在更新Fiber时执行,下面让笔者带你进入代表性hook的世界.
- 1、首先和其他hook一样,都是拿到当前Fiber上的下一个hook.
const hook = updateWorkInProgressHook(); const queue = hook.queue; 复制代码
- 2、还记得我们之前的
lastRenderedReducer
吗,它之所以叫这个名字就是表示,它的reducer是依赖与最后一个reducer函数的.
queue.lastRenderedReducer = reducer; 复制代码
- 那么baseQueue还记得吗?这里笔者油爆枇杷一下,可以看到下面代码,我们讲上一次更新的baseQueue和pending解开链表,装到了baseQueue上,这就意味着此时,baseQueue上已经承载了所有update.
const current: Hook = (currentHook: any); // 上次未更新完的queue let baseQueue = current.baseQueue; // 本次更新的hook const pendingQueue = queue.pending; if (pendingQueue !== null) { if (baseQueue !== null) { // 合并本次更新和上次更新 const baseFirst = baseQueue.next; const pendingFirst = pendingQueue.next; baseQueue.next = pendingFirst; pendingQueue.next = baseFirst; } // 更新baseQueue current.baseQueue = baseQueue = pendingQueue; queue.pending = null; } 复制代码
此时我们已经拿到了updateQueue,接下来就是遍历,但是如果您对React有一定的了解,那么就一定听说过React优先级调度问题
if (baseQueue !== null) { const first = baseQueue.next; let newState = current.baseState; let newBaseState = null; let newBaseQueueFirst = null; let newBaseQueueLast = null; let update = first; // 遍历循环链表 do { // 拿到优先级 const updateLane = update.lane; // 判断渲染优先级和更新优先级,如果渲染优先级不比更新优先级大 // 则本次update放入baseQueue中. if (!isSubsetOfLanes(renderLanes, updateLane)) { const clone: Update<S, A> = { lane: updateLane, action: update.action, hasEagerState: update.hasEagerState, eagerState: update.eagerState, next: (null: any), }; if (newBaseQueueLast === null) { newBaseQueueFirst = newBaseQueueLast = clone; newBaseState = newState; } else { newBaseQueueLast = newBaseQueueLast.next = clone; } // 将本次更新优先级合并 currentlyRenderingFiber.lanes = mergeLanes( currentlyRenderingFiber.lanes, updateLane, ); markSkippedUpdateLanes(updateLane); } else { // 再一次clone if (newBaseQueueLast !== null) { const clone: Update<S, A> = { lane: NoLane, action: update.action, hasEagerState: update.hasEagerState, eagerState: update.eagerState, next: (null: any), }; newBaseQueueLast = newBaseQueueLast.next = clone; } // 没啥用, 兼容setState,下面讲 if (update.hasEagerState) { newState = ((update.eagerState: any): S); } else { // 进入lastRenderedReducer,循环执行 const action = update.action; newState = reducer(newState, action); } } update = update.next; } while (update !== null && update !== first); if (newBaseQueueLast === null) { newBaseState = newState; } else { newBaseQueueLast.next = (newBaseQueueFirst: any); } // 判断是否需要更新 if (!is(newState, hook.memoizedState)) { markWorkInProgressReceivedUpdate(); } hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } 复制代码
- 笔者在上文中已经做了注释,有个重要的点笔者讲一下,可以看到在源码中,对于优先级较低的以及baseQueue不为空的情况,会将本次更新放入baseQueue,那么对于baseQueue不为空的情况都要将update放入队列,可能有点疑惑,笔者想说的是
「React的每次更新都是依赖当前环境做出的变化」
,而如果当在某次更新中你跳过了优先级低的updateA,而执行了优先级高的updateB,然后又跳过了优先级低的updateC,那么在下次执行时就不是理想的「A->B->C」,而是「A->C」,这也是React为什么baseQueue不为空就需要将当前update放入 - 那这不是多此一举吗,下次还是要执行全部,是的,但是如果你从用户角度思考,你是希望快速看到响应的,React在保证快速响应的同时又保证了数据的正确性,者就是著名的
心智模式
笔者在解读该篇时,有跳过优先级处理和cocurrent模式下的交叉渲染,我们后(现)面(在)复(不)盘(会).
useRef
- useRef是一种近乎原生hook,就是将hook的状态指向了某一个值,它用对象的方式,将它放存在堆内存中,保证指向和修改正确
MountRef
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
复制代码
updateRef
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
复制代码
useState
- 这可能是用的最多的hook了,ReactHook以它为状态代替了类组件的state.在源码上它其实借鉴了useReducer的思路,笔者这里只写一下细微的差别吧,不想水代码了.
mountState
- mountState和mountReducer相比,无非是允许useState参数为函数,可以判断初始化,而对于useReducer而言,则将函数的情况放了最后一个参数(init),通过判断最后一个参数,将初始值作为init的参数放入后,将出参作为参数返回.
if (typeof initialState === 'function') {
initialState = initialState();
}
复制代码
upDateState
- 看到这里想必你已经懵了,它居然一丝不挂的抄了useReducer.
- 作为最为熟知的Hook,我们还是稍微体面的说一下它的原理的.
function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { return updateReducer(basicStateReducer, (initialState: any)); } function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { // $FlowFixMe: Flow doesn't like mixed types return typeof action === 'function' ? action(state) : action; } 复制代码
- 如果你对useReducer比较熟知,那么你应该知道在这里更新状态时的useReducer的第一个参数是没有意义的,真正有用的都放在了hook队列中
- 不知道你在使用setState时,有没有这种场景,你想bactchUpdate某一个state,但是当前设置的state的值是依赖上一次更新的,但是结果却和预计有差别
- 可以看下面代码, 当我们以值的形式更新时,它是不满足我们的预期的, 但是当我们使用函数式的时候, 则是满足预期的
const [value, setValue] = useState(1); <button onClick={() => { setValue(value+1); // 2 setValue(value+1); // 2 // setValue((value) => value+1); // 3 }}></button> useEfect(() => { console.log(value); }, [value]) 复制代码
- 原因就在updateReducer的下面这句话,可以看到我们的
basicStateReducer
中会判断传入action类型,如果是值则直接赋值,如果是函数则将本次循环之前设置的值(「newState」)作为参数返回.newState = reducer(newState, action); 复制代码
&&useTransition&&
-
该hook花了笔者大量的时间,由于其相关到了优先级篇,难度较高,如果您并没有那么希望深耕源码,笔者甚至并不建议您读下去,结合网络的资料,笔者并不认为有大部分的面试官能够了解到这个阶段,如果您有更高的自我追求,那么我们继续.
-
该hook的出现一定程度上是为了解决用户感受问题,
我们知道任务队列中存在render和执行脚本,如果脚本执行过长,就会导致在当前帧下render被覆盖,从而产生卡顿
. -
试想下面场景: 我们的
「输入」
和「由输入产生的页面更新」
,我们其实更在意的是输入实时,而数据更新延时是可以接受的,但正常逻辑下这两次执行是同时的,也就是都会进入SyncQueue.那么该hook的出现就是为了解决该问题.(仔细思考,其实这已经涉及到了优先级的问题了.), -
mountTransition
:
function mountTransition(): [boolean, (() => void) => void] {
const [isPending, setPending] = mountState(false);
// The `start` method never changes.
// startTransition见下文
const start = startTransition.bind(null, setPending);
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [isPending, start];
}
// 更新时并没有多余的操作
function updateTransition(): [boolean, (() => void) => void] {
const [isPending] = updateState(false);
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
return [isPending, start];
}
复制代码
- 那么设置transition = 1的作用是什么,又是如何做到延迟执行的呢.
- 我们来大篇幅的剖析一下.以下面的Fiber为例
const App = function() {
const [value, startTransition] = useTransition();
const [v,setV] = useState(0);
const [m,setM] = useState(0);
return <div>
<button onClick={() => {
debugger
startTransition(() => {
setV(1);
})
setM(123);
}}>{v}</button>
{m}
</div>
}
复制代码
- 以此为例: 点击button,界面的渲染过程是(0 0) -> (0 123) -> (1 123);『如果不是这样的话,我们就在讲废话了.』
- 下面笔者撕一下渲染过程.
- 1、首先当前点击会作为
离散事件优先级执行调度
.
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent
) {
// 通过事件优先级 映射 更新优先级.
setCurrentUpdatePriority(DiscreteEventPriority); // 1
// 执行事件
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}
复制代码
- 2、我们跳过batchUpdate等内容,
function startTransition(setPending, callback) {
// 获取当前优先级
const previousPriority = getCurrentUpdatePriority(); // 1
// 我们给当前setPending至少持续事件优先级(对应mousemove等持续事件,小于点击等离散事件优先级)
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority),
);
// 设置pending的优先级是较高的.
setPending(true);
// 这里看到我们设置了一个全局过渡标记位
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
// 这里执行的时候我们的标记量为trnasition
setPending(false);
callback();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
复制代码
- 3、setPending(true)发起第一次调度.
function dispatchSetState(fiber, queue, action) {
requestUpdateLane(fiber) // 浅撕一下
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
// .... 略去不相关过程
enqueueUpdate$1(fiber, queue, update); // 更新循环队列queue.
// 准备调度更新, 这个调度我们也展开一下
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
// 对于transitionLane进行的操作.
entangleTransitionUpdate(root, queue, lane); // ?
}
}
function entangleTransitionUpdate(root, queue, lane) {
if (isTransitionLane(lane)) {
var queueLanes = queue.lanes;
// 当前车道和该stateQueue的车道求(&),获取到queue上存在的本次车道的更新
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
// 将当前过渡车道合并到新的队列车道
var newQueueLanes = mergeLanes(queueLanes, lane);
queue.lanes = newQueueLanes;
// 车道缠绕,将当前车道(|)到root.entangledLanes.
// 再将entangledLanes处理后展开的所有车道缠绕上当前的lane
markRootEntangled(root, newQueueLanes);
}
}
function requestUpdateLane(fiber) {
var mode = fiber.mode; // 标记当前fiber的更新模式,这里我们用了renderRoot,所以是并发模式
..... // 由于我们不是优先级篇,所以这里我们暂时不展开
// 这里就是在拿全局的过渡状态了.
var isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (currentEventTransitionLane === NoLane) {
// 拿到过渡方式的事件车道
// 下面的函数,我们暂时不进入了, 希望您自己去撕,这里就是在拿当前过渡优先级
// 该值起始值为Ob0001000000(64),为了保证唯一性,会不断左移,峰值时环状赋值
// 值得注意的是,该值大于所有事件调度lanes(优先级低于所有事件调度lanes)
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
// 当前更新优先级, 我们在之前已经set,所以这里直接拿到“1”
var updateLane = getCurrentUpdatePriority();
if (updateLane !== NoLane) {
return updateLane;
}
var eventLane = getCurrentEventPriority();
return eventLane;
}
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
checkForNestedUpdates(); // 判断更新次数
// 标记当前节点的lanes和自其以上的所有节点的childLanes,
// 优化查询在更新时从rootFiber以下的待更新节点
var root = markUpdateLaneFromFiberToRoot(fiber, lane);
// 增加根节点的pendingLanes.
// 并且在这里我们会将本次优先级(根据优先级的高低)通过巧妙的位运算
// 作为索引设置root.eventTimes(车道过期事件列表)
markRootUpdated(root, lane, eventTime);
// 调度核心函数. 我们展开一下?***
ensureRootIsScheduled(root, eventTime);
}
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackNode = root.callbackNode; // 当前准备调度“任务“
// 遍历pendinglanes,通过keyValue获取不同类型车道的任务过期事件.
// 设置root.expirationTimes中
// 对于已经过期的任务,将车道作或运算到root.expiredLanes
markStarvedLanesAsExpired(root, currentTime);
// 拿到下一个任务调度车道, 后面分析
var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
// 本次确认调度的任务优先级(获取最高优先级)
var newCallbackPriority = getHighestPriorityLane(nextLanes);
// 当下的任务优先级
var existingCallbackPriority = root.callbackPriority;
// 对于两次任务优先级一致的调度,后续合并调度
if(newCallbackPriority === existingCallbackPriority) ...return;
// 对于存在的紧急任务, 取消等待调度的concurrent任务
if (existingCallbackNode != null) {
cancelCallback$1(existingCallbackNode);
}
// 任务节点
var newCallbackNode;
// 执行到这里时,1、任务调度队列位空, 2、本次任务调度优先级较高.
if(newCallbackPriority === SyncLane) {
if (root.tag === LegacyRoot) {
// 对于非concurrent模式下的任务
// 我们会设置includesLegacy为true,并将任务放入SyncQueue(紧急调度队列)
// 该标记的作用是在执行本次事件batchUpdate后的finally中会判断执行
// includesLegacy为true时,直接遍历syncQueue.
if ( ReactCurrentActQueue$1.isBatchingLegacy !== null) {
ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// concurrent模式下,includesLegacy为false, 也放入SyncQueue中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
{
// Flush the queue in a microtask.
if ( ReactCurrentActQueue$1.current !== null) {
ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
} else {
// 将调度SyncQueue放入微任务队列
scheduleMicrotask(flushSyncCallbacks);
}
} else{
// 任务调度优先级
var schedulerPriorityLevel;
// 我们拿最紧急车道换取事件优先级 -> 映射任务调度优先级
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdlePriority;
break;
default:
schedulerPriorityLevel = NormalPriority;
break;
}
// 等待调度的任务节点scheduleTask.
// 其中实际是执行了unstable_scheduleCallback.
// 通过调度优先级映射过期时间,形成一个task.
newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
}
复制代码
- 1、dispatchSetState
Pending
= true. - 2、设置transition = 1,
标记后续更新为过渡更新
.- 2.1、获取优先级为
过渡优先级
,同时本次update.lane为过渡优先级.
- 2.1、获取优先级为
- 3、在执行
performSyncWorkOnRoot
时(由lane === 1触发的更新),继续执行renderRootSync(beginWork过程)
,其中会执行component(),在再一次updateTransition时,拿到在该Fiber上的相应hook,这时在该hook.queue的update.lane会再一次传染给当前Fiber.lane.(去除update.lane和本次renderLane一致的lane) - 4、在commitRootImpl后,会再一次发起ensureRootIsScheduled确定下一个最紧急的车道lane.
- 5、之后将该车道映射相应的任务以及执行时机,发起异步调度.这就很好的做到了
延时调度和任务插队
–ReactV17+下的事件机制–
笔者在complete的过程中花了大量的事件去思考事件机制,不的不说这一块的资料真心不多,我们进入正题.
- React为了实现多平台API的统一性,实现了自己的一套事件驱动机制.它将所有事件的驱动都放到了自己的顶层节点上,以观察者模式实现内容分发.
- React17相比于React16的差别就是事件被绑定到了RootContainer上,而不是document上,这么做的好处是避免了页面中多个React应用的引起的事件冲突.
- 接下来我们从React.render上的主函数的事件监听入口开始探索源码
- 前置数据.
- 首先我们进入到该文件内
/react-dom/src/events/DOMPluginEventSystem.ts
,该文件是事件调度的核心文件.
下面被横线以内的数据可以暂时跳过,在后面提到时回头看.
-
// 注册ReactName->domEventName的映射对象 SimpleEventPlugin.registerEvents(); // 注册普通事件 EnterLeaveEventPlugin.registerEvents(); // 鼠标移动事件, onMouseEnter. ChangeEventPlugin.registerEvents(); // 状态改变事件,onChange SelectEventPlugin.registerEvents(); // 注册onSelect BeforeInputEventPlugin.registerEvents(); // 注册输入之前事件,不常使用 复制代码
- 这里的每一个rigisterEvents对应一个文件那的事件注册,接下来我们进入simpleEventPlugin来窥探一下内部实现.
-
const simpleEventPluginEvents = [ // 原生事件数组 'abort', 'auxClick', 'cancel', 'canPlay', 'canPlayThrough', 'click', ]; // 注册函数 export function registerSimpleEvents() { for (let i = 0; i < simpleEventPluginEvents.length; i++) { // 这里我们在创建原生事件和React事件,并进入真正的注册函数. const eventName = ((simpleEventPluginEvents[i]: any): string); const domEventName = ((eventName.toLowerCase(): any): DOMEventName); const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1); registerSimpleEvent(domEventName, 'on' + capitalizedEvent); } // Special cases where event names don't match. registerSimpleEvent(ANIMATION_END, 'onAnimationEnd'); registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration'); registerSimpleEvent(ANIMATION_START, 'onAnimationStart'); registerSimpleEvent('dblclick', 'onDoubleClick'); registerSimpleEvent('focusin', 'onFocus'); registerSimpleEvent('focusout', 'onBlur'); registerSimpleEvent(TRANSITION_END, 'onTransitionEnd'); } // 进入 function registerSimpleEvent(domEventName, reactName) { // map注册当前dom下的事件. topLevelEventsToReactNames.set(domEventName, reactName); // 事件注册 registerTwoPhaseEvent(reactName, [domEventName]); } // export function registerTwoPhaseEvent( registrationName: string, dependencies: Array<DOMEventName>, ): void { // 这里我们可以看到同时注册了捕获和冒泡事件. registerDirectEvent(registrationName, dependencies); registerDirectEvent(registrationName + 'Capture', dependencies); } export function registerDirectEvent( registrationName: string, dependencies: Array<DOMEventName>, ) { // 这里是ReactName-> domEventName的映射表 registrationNameDependencies[registrationName] = dependencies; // 这里我们存储了所有原生事件 for (let i = 0; i < dependencies.length; i++) { allNativeEvents.add(dependencies[i]); } } 复制代码
listenToAllSupportedEvents
- 该函数是监听所有被支持事件的入口.(V17的做出的变化)
- 该函数会将所有原生事件绑定到rootContianer上,统一控制事件分发.
-
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) { // 判断当前节点是否开启监听状态 if (!(rootContainerElement: any)[listeningMarker]) { (rootContainerElement: any)[listeningMarker] = true; // 遍历所有原生事件监听列表,这里可以我们可以回头查看我们之前说到的. allNativeEvents.forEach(domEventName => { if (domEventName !== 'selectionchange') { // 对于不会冒泡的事件不绑定冒泡监听 if (!nonDelegatedEvents.has(domEventName)) { // 重点函数 listenToNativeEvent(domEventName, false, rootContainerElement); } // 绑定捕获监听. listenToNativeEvent(domEventName, true, rootContainerElement); } }); const ownerDocument = (rootContainerElement: any).nodeType === DOCUMENT_NODE ? rootContainerElement : (rootContainerElement: any).ownerDocument; if (ownerDocument !== null) { if (!(ownerDocument: any)[listeningMarker]) { (ownerDocument: any)[listeningMarker] = true // 该事件只能注册到document上 listenToNativeEvent('selectionchange', false, ownerDocument); } } } } 复制代码
listenToNativeEvent
- 该函数实现事件注册到目标节点.
-
export function listenToNativeEvent( domEventName: DOMEventName, isCapturePhaseListener: boolean, target: EventTarget, ): void { let eventSystemFlags = 0; if (isCapturePhaseListener) { // 标记捕获 eventSystemFlags |= IS_CAPTURE_PHASE; } // 再次进入 addTrappedEventListener( target, domEventName, eventSystemFlags, isCapturePhaseListener, ); } 复制代码
- 「
addTrappedEventListener
」(事件重点)- 根据事件优先级创建回调并并注册
-
function addTrappedEventListener( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport?: boolean, ) { // 创建回调,这里我们重点关注一下. let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, ); // 对于部分可能会由于持续触发而占用主线程的进行判定 // 因为浏览器无法知道回调是否存在preventDefault而进行滚动 // 所有会等待回调执行,那么就可能会引起掉帧. // 该参数就就是告诉浏览器,我的回调里不存在preventDefault. let isPassiveListener = undefined; if (passiveBrowserEventsSupported) { if ( domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel' ) { isPassiveListener = true; } } let unsubscribeListener; // 设置监听事件addEventListener // 捕获以及isPsssive判定和回调绑定 if (isCapturePhaseListener) { if (isPassiveListener !== undefined) { // 下列的add函数就是进入事件监听,我们不下去看了. unsubscribeListener = addEventCaptureListenerWithPassiveFlag( targetContainer, domEventName, listener, isPassiveListener, ); } else { unsubscribeListener = addEventCaptureListener( targetContainer, domEventName, listener, ); } } else { if (isPassiveListener !== undefined) { unsubscribeListener = addEventBubbleListenerWithPassiveFlag( targetContainer, domEventName, listener, isPassiveListener, ); } else { unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); } } } 复制代码
createEventListenerWrapperWithPriority
, 判定事件优先级,创建监听回调.- React将事件分为离散事件、持续事件、默认事件优先级.『执行顺序从高到低』
-
export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, ): Function { // 根据事件名获取优先级 const eventPriority = getEventPriority(domEventName); let listenerWrapper; // 根据事件优先级设置不同的事件,让他们入不同事件优先级的队列. switch (eventPriority) { case DiscreteEventPriority: listenerWrapper = dispatchDiscreteEvent; break; case ContinuousEventPriority: listenerWrapper = dispatchContinuousEvent; break; case DefaultEventPriority: default: listenerWrapper = dispatchEvent; break; } // 创建事件分发函数, 套在事件监听器之中, 让React决定调度 return listenerWrapper.bind( null, domEventName, eventSystemFlags, targetContainer, ); } 复制代码
dispatchEventsForPlugins
,这里我们跳过中间的一系列处理过程,进入关键调度.- 该函数会进行
合成事件创建
和事件冒泡执行
等操作. - ->
function dispatchEventsForPlugins( domEventName: DOMEventName, // 原生事件名 eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, // 原生事件参数e targetInst: null | Fiber, // 目标Fiber targetContainer: EventTarget, // currentTarget ): void { // 获取currentTarget const nativeEventTarget = getEventTarget(nativeEvent); const dispatchQueue: DispatchQueue = []; // dispatchQueue中放置对应事件名的事件列表 // 该函数进入分发队列进行赋值 // 在该函数中,我们首先会switch(domEventName)合成不同事件,使多平台事件标准化 // 然后我们会根据targetInst(目标Fiber)回溯Fiber树-> // 1、从而获取dom树并将其Dom指向合成事件的currentTarget并在调度中作为参数传入, // 2、获取所有上层Fiber的props中和当前ReactEventName相符的回调并传入DispatchEvents中. // 3、DisPatchEvents中从头到尾是子到父的排列.(后续有用) extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); // 该队列的每一个对象表示由该事件触发「冒泡或捕获」的所有回调函数列表 // Queue [{instance: null,listeners: callbackArr,event: SyntheticFocusEvent}]; // 该函数,会对event的是否阻止冒泡以及捕获等进行判断,然后遍历分发列表进行调度. // 冒泡,从头到尾执行, 捕获, 从尾到头执行. processDispatchQueue(dispatchQueue, eventSystemFlags); } 复制代码
- 该函数会进行
笔者这里空出了
processDispatchQueue
和extractEvents
以及合成事件函数希望您能自行翻译,主要原因是这些内容嵌套较多,而且比较核心,难度也并不高,希望您主动阅读.
- 下面我们捋一下全过程
- 1、首先在ReactEvent文件中我们注册了domEventName->ReactEventName的映射、allNativeEvents等前置数据
- 2、遍历allNativeEvents并对于相应原生事件的冒泡(特殊不冒泡做相应处理)和捕获两状态设置回调(见下一步),并注册监听器到
RootContainer
上 - 3、根据ReactEventName获取相应事件优先级,进入不同调度队列
- 5、根据原生e.targetEvent(兼容性这里不多写了),拿到internalInstanceKey属性上的Fiber映射
- 6、回溯Fiber树,拿到所有祖先层Fiber的props和stateNode(对应dom)
- 7、合成listeners, 例listeners: [{listener: Fn1, currentTarget: Fn1被放置的dom}]
- 4、根据当前
ReactEventName
,是否冒泡
,合成事件SyntheticEvent - 5、合成dispatchEvents存储所有事件调度, 例[{event: SyEvent1, listeners: ls1}]
- 6、遍历dispatchEvents,其中每一个合成事件对应多个回调,遍历listeners,执行回调并将currentTarget赋给
e
作为回调参数默认传入然后执行
总的来说事件这一块还是有一定挑战,原因是V17将合成事件进行了重写,导致网上资料较少,并且前置条件和嵌套多,笔者并没有将关注点放在合成事件以外的地方(自己也有地方不熟悉?) 但是也算是打开了笔者的源码大门,冲!!!
–Diff算法–
Diff算法可以说是源码的入门考验,如果你敢在简历上写下你熟悉React源码,那么这个不会你就可以走人了.当然你了解的深度可能也决定了你的评级
「由于$$typeof类型众多,我们仅考虑REACT_ELEMENT_TYPE」
场景: 该过程发生在beginWork阶段, 在该阶段会进行新旧Fiber的对比及更新
单节点比较
- 这里使用Fiber和newChild进行比较协调,因为我们现在需要创建的新Fiber树(woekInProgress Fiber树),是需要通过
JSX创建的ReactElement对象以及上次更新的Fiber节点
来获得的.-
case REACT_ELEMENT_TYPE: // 入口函数 return placeSingleChild( reconcileSingleElement( returnFiber, // 表示returnFiber节点,也就是父Fiber currentFirstChild, // 上一次更新Fiber,通过sibling形成单链表 newChild, // ReactElement lanes, // 优先级「这一节我们不考虑」 ), ); 复制代码
- reconcileSingleElement函数是通过遍历currentFirstFiber链表来决定是否存在可利用的节点,请看代码「省去了与Diff无关的代码」
-
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, lanes: Lanes, ): Fiber { const key = element.key; let child = currentFirstChild; // 上一次更新的头节点 // 遍历 while (child !== null) { // 判断key值 if (child.key === key) { const elementType = element.type; // 判断元素标签 if (child.elementType === elementType) { // 移除以第二个参数为首的后续链表节点 // 在父节点对象上的deletions数组「标记需要删除的子节点数组」 // 在子节点的flags上通过位运算标记该节点需要移除 deleteRemainingChildren(returnFiber, child.sibling); // 复用节点,并传入新props const existing = useFiber(child, element.props); // 移动ref指向 existing.ref = coerceRef(returnFiber, child, element); // 父节点指向. existing.return = returnFiber; return existing; } } // 因为key存在唯一性,如果key相等但是元素类型变了,那么后续就不需要遍历直接清空. deleteRemainingChildren(returnFiber, child); break; } else { // 移除当前节点 deleteChild(returnFiber, child); } child = child.sibling; } // 不可复用,新建. const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; } 复制代码
- 判断新建节点,如果
节点未与returneFiber连接
, 标记PlaceMent(新增节点) -
function placeSingleChild(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; } return newFiber; } 复制代码
-
多节点比较
- 多节点比较相较于单节点更加复杂.
-
if (isArray(newChild)) { // 判断节点数组 return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, lanes, ); } 复制代码
我们需要考虑两条链表以最少的时间复杂度,实现可复用节点的查找与拼接
- React的Diff思路是,「实现两次遍历」
- 第一次按序一比一遍历,如果节点皆可复用,那么就不需要多余的操作
- 如果旧节点复用完成而新节点依旧存在,那么证明可复用的结束,剩下全部新建.
- 如果新节点复用完成而旧节点依然存在,那么旧节点只需要删除.
如果第一次遍历中按序遍历,遇到无法复用,中断第一次遍历
,进入第二次遍历,将oldFiber以Fiber.key为key(如果不存在,以Fiber.index为key),以Fiber为value,建立map
.如果newChild.key可以被map索引,那么证明存在复用节点,反之,进行新建.(开始源码)
变量相关
- // diff 算法
// 记录经过diff之后新的Fiber链表头部
let resultingFirstChild: Fiber | null = null;
// 记录上一次遍历的newFiber节点
let previousNewFiber: Fiber | null = null;
// 记录当前的旧节点
let oldFiber = currentFirstChild;
// 在oldFiber中的最后一个可复用的下标
let lastPlacedIndex = 0;
// 记录新节点的下标
let newIdx = 0;
// 记录下一次比较的旧节点
let nextOldFiber = null;
复制代码
第一次遍历
第一次遍历的目的是, 对于未更改的ArrayFiber,通过O(n)直接遍历完成,不需要进行多余的操作,否则立刻break,将复杂操作交给第二次遍历.
这里笔者发现大部分文章错误的地方「也可能是React进行优化的地方」,对于不定义key的ReactElement,React并没有create, 而是依然进行elementType、key(null)的比较,实现复用(虽然控制台会throw Error),当然, 笔者也通过实践确定了该观点,而这与之前笔者看到的大部分描述相悖,也充分证明了阅读源码的必要性
-
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { // 老节点的索引比新节点小了,需要插入新的节点,并且固定旧节点进行比较. if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } // 进入Fiber和child比较,决定是否复用, 如果不可复用返回null const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], lanes, ); // 当前不可复用, 退出第一次遍历 if (newFiber === null) { if (oldFiber === null) { oldFiber = nextOldFiber; } break; } // 定位最后一个可复用下标 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } 复制代码
- 这里我们需要谈论一下placeChild函数,该函数是通过
比较在该次遍历以前的最后一个可复用节点的位置和当前newIdx进行比较,从而确定是否是否存在节点移动,React会通过shouldTrackSideEffects变量确定「更新」或者「新建」,如果是新建那么只会在第一个Fiber上标记Forked,而不是对每一个Fiber都标记placeMent,这么做的目的是提高性能
- 这里我们需要谈论一下placeChild函数,该函数是通过
我们不具体展开「全部复用」,「oldFiber全部复用剩下newChild新建」, 「newChild全部复用剩余oldFiber移除问题」情况, 这些情况是可预测的.
第二次遍历
到了这里, 表示oldFiber与newChildren都未遍历完成, 这里可预测两种情况, 1、newChildren被移动,但是依然可复用. 2、节点被插入
-
// 建立oldFiber.key为key「null时,用index代替」,oldFiber为value的键值对. const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // 从第一次未遍历完成的下标开始. for (; newIdx < newChildren.length; newIdx++) { // 从oldFiber Map中查找newChildren是否存在,以O(1)确定节点是否被移动 const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); if (newFiber !== null) { // 更新状态下 if (shouldTrackSideEffects) { // 如果Fiber可重用 if (newFiber.alternate !== null) { // 从键值对中移除对应的oldFiber,(该map后续有用处) existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key, ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 链表移动 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } // 更新状态下, 我们需要通过oldFiber Map进行标记delete,这也是为什么之前将可复用Fiber移除Map. if (shouldTrackSideEffects) { existingChildren.forEach(child => deleteChild(returnFiber, child)); } 复制代码
- 补充: 对于Diff中确定可复用节点是比较重要的点, 我们稍微展开一下.
- 这里我们以oldFiber: A->B->C->D「箭头表示sibling指向」, newChildren: A’->C’->B’->D'(A’表示Fiber A对应的ReactElement).
- 首先第一次遍历时,我们确定A可直接复用,B与C’不对应,且两个链表都未遍历完.此时placeIndex = 0(最后一个可复用节点下标),
- 进入第二次遍历,
建立了BCD相关的map
, 从中得出C’对应C的oldFiber Index = 2, 2 > placeIndex, 我们重新定义placeIndex = 2,该节点依然不需要移动, 并且将C从oldFiber map中移除. - 下一个B’节点对应的Index = 1, 1 < placeIndex, 该节点需要移动, 但依然可复用,通过alterate进行相互绑定,并为B标记PlaceMent表示需要移动,placeIndex依然为2.并从map中删除B
- 下一个C’的index = 3,3 > placeIndex,节点不需要移动,继续从map中删除C, 遍历结束.
总结: Diff的资源是比较多的,很多文章都有对它的介绍,并且它的耦合度也是较低的,是可以拿出来单独翻的一块,当然也是需要了解大致流程的,不过看再多的文章也比不过手撕,所以还是希望读者可以去翻一下,这样你可以很自信的了解Diff流程,本文对链表的操作没有着重阐述,因为默认读者是有数据结构基础的,接下来我们继续?.
参考资料: