Reconciliation(diff)算法
调和的目的
在用户无感知的情况下将数据的更新体现到UI上。
触发调和过程的方式
-
ReactDom.render()函数 和ReactNativeRenderer.render()函数
-
setState()、forceUpdate()、componentWillMount 、componentWillReceiveProp 中直接修改了state
-
hooks 中的useReducer 和 useState 返回的钩子函数
调和的工作原理
render 阶段
构建 Fiber 对象,构建链表,在链表中标记要执行的 DOM 操作 ,可中断。
const fiber = { stateNode,// dom节点实例 child,// 当前节点所关联的子节点 sibling,// 当前节点所关联的兄弟节点 return// 当前节点所关联的父节点 }
-
支持暂停,终止以及恢复之前的渲染任务
-
支持增量渲染,
fiber
将react
中的渲染任务拆分到每一帧 -
通过
fiber
赋予了不同任务的优先级当数据发生改变引起页面组件进行更新,进入diff算法:
// diff对比过程其实也是在通过链表进行递归 function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. // 只要还有节点单元,一直进行对比 while (workInProgress !== null) { performUnitOfWork(workInProgress); } } function performUnitOfWork(unitOfWork) { // 获取当前fiber节点 var current = unitOfWork.alternate; setCurrentFiber(unitOfWork); // 创建next节点,等会会设置next为下一个要对比的fiber节点 var next; if ( (unitOfWork.mode & ProfileMode) !== NoMode) { // 设置fiber节点的开始时间 startProfilerTimer(unitOfWork); // 获取当前fiber节点的child,将其设置为next next = beginWork$1(current, unitOfWork, subtreeRenderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { next = beginWork$1(current, unitOfWork, subtreeRenderLanes); } resetCurrentFiber(); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { // 将next赋予给workInProgress,于是while循环会持续进行 workInProgress = next; } } //在workLoopSync方法中可以看到while (workInProgress !== null)的判断,只要fiber节点不为空,就一直递归调用performUnitOfWork方法。 //而在performUnitOfWork中可以看到前文我们说的链表的概念,react通过next = 当前节点child的操作,只要子节点仍存在,就不断更新next并赋予给workInProgress,所以也验证了前文所说,即便任务被暂停,react也能通过next继续先前的工作。
Reconciliation
协调阶段
// current -- 旧有的fiber节点信息 // workInProgress -- 也是旧有的fiber节点信息,结构与current有少许不同 // nextChildren -- 之前调用Component(props, secondArg)得到的虚拟dom子节点 function reconcileChildren(current, workInProgress, nextChildren, renderLanes) { // 通过current我们能知道此时是初次渲染,还是更新 if (current === null) { // 挂载fiber节点 workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); } else { // diff fiber节点 workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes); } } // reconcileChildFibers方法会判断新的节点是什么类型,比如当前我们传递的是虚拟dom div,它是个对象,所以会继续调用placeSingleChild方法,根据递归的特性,等会还会对比div的props function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) { // 判断传递的新虚拟dom是不是对象 var isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 是对象,且是虚拟dom类型,继续调用 return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)); } } // 删除部分无用代码 if (isArray$1(newChild)) { // 若有子节点 return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes); } }
// 参数与上个方法的参数注解相同,按值传递 function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) { // 获取新虚拟dom的key var key = element.key; // 旧有的div fiber节点 var child = currentFirstChild; // 判断旧有fiber存不存在,一定是存在才能diff,否则就是走fiber创建初始化了 while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { switch (child.tag) { // 删除部分无用逻辑 default: { if (child.elementType === element.type || ( isCompatibleFamilyForHotReloading(child, element) )) { deleteRemainingChildren(returnFiber, child.sibling); // 根据新的虚拟dom的props来更新旧有div fiber节点 var _existing3 = useFiber(child, element.props); // 更新完成后重新设置ref以及父节点 _existing3.ref = coerceRef(returnFiber, child, element); _existing3.return = returnFiber; return _existing3; } break; } } deleteRemainingChildren(returnFiber, child); break; } else { // 如果key不相等,直接在父节点上把自己整个都删掉 deleteChild(returnFiber, child); } // 将兄弟节点赋予给child,继续while遍历 child = child.sibling; } // 如果不存在旧的fiber节点,那说明是挂载,因此否则走fiber的初始化 // 这里的初始化我删掉了 } // 1判断是否存在旧有的fiber节点,如果不存在说明没必要diff,直接走fiber新建挂载逻辑。 // 2有child说明有旧有fiber,那就对比key,如果不相等,直接运行deleteChild(returnFiber, child),也就是从div节点的旧有父节点上,将整个div都删除掉,div的子节点都不需要比了,这也验证了react的逐级比较,父不同,子一律都不比较视为不同。 // 3若key相同,那就比较新旧fiber的type(标签类型),如果type不相同,跟key不相同一样,调用了deleteRemainingChildren(returnFiber, child)方法,直接从div的旧有父节点上将自己整个删除。 // 4若key type都相同,那只能说明是props变了,因此调用var _existing3 = useFiber(child, element.props)方法,根据新的props来更新旧有的div fiber节点。 function useFiber(fiber, pendingProps) { // 使用旧有的fiber节点以及新的props来创建一个新的clone fiber var clone = createWorkInProgress(fiber, pendingProps); clone.index = 0; clone.sibling = null; return clone; }
commit 阶段
根据构建好的链表进行 DOM 操作,不可中断。
function performSyncWorkOnRoot(root){ // 删除意义不大的代码 var finishedWork = root.current.alternate; root.finishedWork = finishedWork; root.finishedLanes = lanes; // 提交root节点 commitRoot(root); // Before exiting, make sure there's a callback scheduled for the next // pending level. ensureRootIsScheduled(root, now()); return null; } function commitRoot(root) { // 这里会对当前任务进行优先级判断,再决定后续处理 // 断点发现这里的优先级是99,最高优先级 var renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority$1(ImmediatePriority$1, commitRootImpl.bind(null, root, renderPriorityLevel)); return null; }
fiber是一种数据结构,使用父子关系以及next的妙用,以链表形式模拟了传统调用栈。 fiber是一种调度让出机制,只在有剩余时间的情况下运行。 fiber实现了增量渲染,在浏览器允许的情况下一点点拼凑出最终渲染效果。 fiber实现了并发,为任务赋予不同优先级,保证了一有时间总是做最高优先级的事,而不是先来先占位死板的去执行。 fiber有协调与提交两个阶段,协调包含了fiber创建与diff更新,此过程可暂停。而提交必须同步执行,保证渲染不卡顿。
而通过fiber的协调阶段,我们了解了diff的对比过程,如果将fiber的结构理解成一棵树,那么这个过程本质上还是深度遍历,其顺序为父—父的第一个孩子—孩子的每一个兄弟。
通过源码,我们了解到react的diff是同层比较,最先比较key,如果key不相同,那么不用比较剩余节点直接删除,这也强调了key的重要性,其次会比较元素的type以及props。而且这个比较过程其实是拿旧的fiber与新的虚拟dom在比,而不是fiber与fiber或者虚拟dom与虚拟dom比较,其实也不难理解,如果key与type都相同,那说明这个fiber只用做简单的替换,而不是完整重新创建,站在性能角度这确实更有优势
React中的4种优先级
UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接关系。不同事件产生的更新,它们的优先级是有差异的,所以更新优先级的根源在于事件的优先级。一个更新的产生可直接导致react生成一个更新任务,最终这个任务被Scheduler调度。
-
事件优先级:按照用户事件的交互紧急程度,划分的优先级
-
更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)
-
任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级
-
调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级
前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制
事件优先级
React按照事件的紧急程度,把它们划分成三个等级:
-
离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。
-
用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。
-
连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。
事件优先级是在注册阶段被确定的,在向root上注册事件时,会根据事件的类别,创建不同优先级的事件监听(listener----dispatchDiscreteEvent、dispatchUserBlockingUpdate、dispatchEvent这三个中的一个),最终将它绑定到root上去。以某种优先级去执行事件处理函数其实要借助Scheduler中提供的runWithPriority函数来实现
function dispatchUserBlockingUpdate( domEventName, eventSystemFlags, container, nativeEvent,) { ... runWithPriority( UserBlockingPriority, dispatchEvent.bind( null, domEventName, eventSystemFlags, container, nativeEvent, ), ); ... }
这么做可以将事件优先级记录到Scheduler中,相当于告诉Scheduler:你帮我记录一下当前事件派发的优先级,等React那边创建更新对象(即update)计算更新优先级时直接从你这拿就好了。
function unstable_runWithPriority(priorityLevel, eventHandler) { switch (priorityLevel) { case ImmediatePriority: case UserBlockingPriority: case NormalPriority: case LowPriority: case IdlePriority: break; default: priorityLevel = NormalPriority; } var previousPriorityLevel = currentPriorityLevel; // 记录优先级到Scheduler内部的变量里 currentPriorityLevel = priorityLevel; try { return eventHandler(); } finally { currentPriorityLevel = previousPriorityLevel; }}
更新优先级
以setState为例,事件的执行会导致setState执行,而setState本质上是调用enqueueSetState,生成一个update对象,这时候会计算它的更新优先级,即update.lane:它首先找出Scheduler中记录的优先级:schedulerPriority,然后计算更新优先级:lane
const classComponentUpdater = { enqueueSetState(inst, payload, callback) { ... // 依据事件优先级创建update的优先级 const lane = requestUpdateLane(fiber, suspenseConfig); const update = createUpdate(eventTime, lane, suspenseConfig); update.payload = payload; enqueueUpdate(fiber, update); // 开始调度 scheduleUpdateOnFiber(fiber, lane, eventTime); ... } }; export function requestUpdateLane( fiber: Fiber, suspenseConfig: SuspenseConfig | null, ): Lane { ... // 根据记录下的事件优先级,获取任务调度优先级 const schedulerPriority = getCurrentPriorityLevel(); let lane; if ( (executionContext & DiscreteEventContext) !== NoContext && schedulerPriority === UserBlockingSchedulerPriority ) { // 如果事件优先级是用户阻塞级别,则直接用InputDiscreteLanePriority去计算更新优先级 lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); } else { // 依据事件的优先级去计算schedulerLanePriority const schedulerLanePriority = schedulerPriorityToLanePriority( schedulerPriority, ); ... // 根据事件优先级计算得来的schedulerLanePriority,去计算更新优先级 lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane; } getCurrentPriorityLevel负责读取记录在Scheduler中的优先级: function unstable_getCurrentPriorityLevel() { return currentPriorityLevel;
update对象创建完成后意味着需要对页面进行更新,会调用scheduleUpdateOnFiber进入调度,而真正开始调度之前会计算本次产生的更新任务的任务优先级,目的是与已有任务的任务优先级去做比较,便于做出多任务的调度决策
任务优先级
一个update会被一个React的更新任务执行掉,任务优先级被用来区分多个更新任务的紧急程度,它由更新优先级计算而来,举例来说:
假设产生一前一后两个update,它们持有各自的更新优先级,也会被各自的更新任务执行。经过优先级计算,如果后者的任务优先级高于前者的任务优先级,那么会让Scheduler取消前者的任务调度;如果后者的任务优先级等于前者的任务优先级,后者不会导致前者被取消,而是会复用前者的更新任务,将两个同等优先级的更新收敛到一次任务中;如果后者的任务优先级低于前者的任务优先级,同样不会导致前者的任务被取消,而是在前者更新完成后,再次用Scheduler对后者发起一次任务调度。
这是任务优先级存在的意义,保证高优先级任务及时响应,收敛同等优先级的任务调度
调度优先级
调度优先级由任务优先级计算得出,一旦任务被调度,那么它就会进入Scheduler,在Scheduler中,这个任务会被包装一下,生成一个属于Scheduler自己的task,这个task持有的优先级就是调度优先级。
在Scheduler中,分别用过期任务队列和未过期任务的队列去管理它内部的task,过期任务的队列中的task根据过期时间去排序,最早过期的排在前面,便于被最先处理。而过期时间是由调度优先级计算的出的,不同的调度优先级对应的过期时间不同。
事件优先级由事件本身决定,更新优先级由事件计算得出,然后放到root.pendingLanes,任务优先级来自root.pendingLanes中最紧急的那些lanes对应的优先级,调度优先级根据任务优先级获取。几种优先级环环相扣,保证了高优任务的优先执行