前言
本次React源码参考版本为17.0.3。这是React源码系列第九篇,建议初看源码的同学从第一篇开始看起,这样更有连贯性,下面有源码系列链接。
终于写到这里了,这算是17版本的终结篇了,后面一篇算是未来篇。
热身准备
系列文章前面讲了渲染,其实主要讲的是初始化,那在后续的交互中会触发视图的更新,React是怎么做的呢?这篇文章带你揭秘React的更新机制。
触发更新的几种场景
用过React的了解触发更新主要是组件的props或者state发生了变化,可能导致他们变化的有几种场景:
- 初次渲染(比如在生命周期中
setState); - 定时任务(定时任务中有
setState); - 事件回调(回调中有
setState); context变化(Context.Provider的value);
我们日常开发中,触发React更新主要还是通过setState的形式,接下来,我们就从setState开始探索React的更新机制。
setState之后
在系列文章的React源码系列之三:hooks之useState,useReducer有讲过,当React执行setState实际上是触发了一个dispatchAction,基于setState的入参创建了一个update,进入更新调度scheduleUpdateOnFiber。
scheduleUpdateOnFiber
在这里,会对当前任务队列中的任务基于当前setState触发时间做判断,如果任务时间比currentTime小,即是一个过期任务,需要在下次更新时立即执行。
markStarvedLanesAsExpired(root, currentTime);
获取当前setState任务的优先级,这一块代码判断就比较复杂了,笔者也没有深入研究
var nextLanes = getNextLanes(root, lanes);
var newCallbackPriority = returnNextLanesPriority();
在做完优先级判断后,根据优先级创建一个回调任务存储在root.callbackNode中,下面主要基于优先级区分同步任务,异步任务还是批量更新任务,以及执行的优先级。
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(ImmediatePriority$1, performSyncWorkOnRoot.bind(null, root));
} else {
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
在scheduleSyncCallback和scheduleCallback中,最后都会调用unstable_scheduleCallback,在这个函数中,会基于优先级计算得出过期时间创建任务
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = 250;
break;
case IdlePriority:
timeout = 1073741830;
break;
case LowPriority:
timeout = 10000;
break;
case NormalPriority:
default:
timeout = 5000;
break;
}
var expirationTime = startTime + timeout;
过期时间久的说明任务优先级比较低,可以放在后面执行,优先级高的任务,过期时间会比当前时间小,这样直接就是过期任务,React会在下次执行任务时立即执行。
任务创建好了,那到底什么时候执行呢?下面这段代码给了我们答案,当任务创建完了,React会通过MessageChannel创建一个宏任务,并且立即port2.postMessage去告知port1执行performWorkUntilDeadline,performWorkUntilDeadline会执行传入的回调函数。
我们暂时不考虑优先级和调度时机的问题,把重心放到它在回调中会执行的函数,我们可以注意到,在这三种任务中,最终传入回调的都是performSyncWorkOnRoot.bind(null, root),也就是说,不管什么时候执行,最终都是执行的performSyncWorkOnRoot,那我们就知道了下步更新的方向了:performSyncWorkOnRoot。
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
// 基于MessageChannel实现异步调度更新, 如果不支持MessageChannel会使用setTimeout
requestHostCallback = function (callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
performSyncWorkOnRoot
创建rootFiber
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 初始化workInProgressRoot, workInProgress, mount创建, update拷贝
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
在prepareFreshStack中,如果有currentfiber树,React会判断为更新阶段,它会从curret上拷贝一份作为workInprogress的rootFiber。
有了rootFiber,接下来就是递归遍历这个rootFiber的子节点,找出和currentfiber树的不同,然后更新,一样要走beginWork,completeWork和commitWork这些渲染时的阶段。
beginWork
和初始化渲染阶段执行的是一个函数,但是会在一些逻辑判断中有所不同。
React判断是初始化渲染阶段还是更新阶段,一个重要的参考是current === null。
React在渲染的beginWork阶段有下面这样一个判断
if (oldProps !== newProps || hasContextChanged() || (workInProgress.type !== current.type )) {
didReceiveUpdate = true;
}
我们可以留意到workInProgress.type !== current.type这个判断条件,这就是在判断元素类型,更新前后的类型是否一致。如果不一致didReceiveUpdate = true;。再看下面代码当didReceiveUpdate = true;时会做什么
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
// clone current fiber并返回
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
这段代码告诉了我们答案, 当current !== null && !didReceiveUpdate时,会直接复制current的值并返回, 反之, 当didReceiveUpdate = true;时,不会进入这个判断,会走下面的重新
假设有个<App />的函数根组件,在beginWork阶段会执行这个函数组件,通过useState拿到新的setState的值来支持后面的更新。
函数组件执行完毕后,就会开始递归遍历函数组件return的React元素,这里就涉及到我们上篇文章React源码系列之八:React的diff算法了,有兴趣的可以去看看,这里就略过了。
completeWork
在completeWork阶段,更新阶段主要是对新老节点的props进行diff,在上篇文章也有介绍。在初始化渲染阶段主要执行appendAllChildren。
如果在更新阶段有新加的React元素还是会执行appendAllChildren,将fiber节点上新增的子节点加入到dom中。
commitWork
在commitWork的mutation阶段,也就是渲染DOM的时候,会将completeWork中diff出来的改动更新的DOM中,此时就是真正的渲染页面了。
新增,删除DOM就不说了,都是按照正常的渲染流程。我们来看下DOM更新
function updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag) {
for (var i = 0; i < updatePayload.length; i += 2) {
var propKey = updatePayload[i];
var propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
React根据completeWork中diff出来props来更新DOM,区分props的类型为style, dangerouslySetInnerHTML, chidren和其他。
style:就是DOM的style;dangerouslySetInnerHTML:React提供的api,用来写DOM节点的(富文本中有用到,慎用),从源码能看到其实使用的就是innerHtml;children:children默认为文本,直接渲染;- 其他
prop:主要通过setAttribute和removeAttribute进行处理;
总结
来总结下setState导致的更新:
- 首先通过
dispatchAction建一个update存储在对应的fiber节点中,然后开始调度更新; - 在调度更新时会判断当前应用中是否有更新任务,然后判断本次
setState任务的优先级来决定更新时机; - 在
setState任务开始执行时,会向初始化渲染一样经历beginWork,completeWork,commitWork; - 在
beginWork,completeWork中,通过diff确定要更新的元素; - 在
commitWork中将diff出来的更新应用到DOM中,渲染到页面;
至此,我们系列文章从React的fiber,到初始化渲染,到hook,到合成事件,diff算法,更新机制都做了剖析,肯定有不全面和不对的地方,而且笔者主要分析的是函数组件和客户端渲染,对整个只能说对React的运行机制有了大概的理解,希望各位看官也能有所收获。
看完这篇文章, 我们可以弄明白下面这几个问题:
React的更新机制是怎样的?
系列文章安排:
- React源码系列之一:Fiber;
- React源码系列之二:React的渲染机制;
- React源码系列之三:hooks之useState,useReducer;
- React源码系列之四:hooks之useEffect;
- React源码系列之五:hooks之useCallback,useMemo;
- React源码系列之六:hooks之useContext;
- React源码系列之七:React的合成事件;
- React源码系列之八:React的diff算法;
- React源码系列之九:React的更新机制;
- React源码系列之十:Concurrent Mode;
参考: