前言
本次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
中,如果有current
fiber树,React
会判断为更新阶段,它会从curret
上拷贝一份作为workInprogress
的rootFiber
。
有了rootFiber
,接下来就是递归遍历这个rootFiber
的子节点,找出和current
fiber树的不同,然后更新,一样要走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;
参考: