React Diff 算法的源碼應(yīng)該怎么讀
這兩個(gè)月時(shí)間密集型的輔導(dǎo)了我的幾個(gè)學(xué)生通過(guò)大廠面試,由于面試強(qiáng)度非常高,因此在準(zhǔn)備面試時(shí),就對(duì)原理這一塊的深度有非常大的要求,不能僅僅停留在八股文的層面去糊弄面試官,回答的東西要禁得起拷打。
于是我就專門又重新花了一點(diǎn)時(shí)間去讀了一下 React 19 的源碼。在讀的過(guò)程中又有了一些新的體會(huì)。
這篇文章準(zhǔn)備給大家分享一個(gè)面試中比較容易被深入探討到的點(diǎn):diff 算法。如果你的薪資訴求非常高,在 30 K 以上,那么對(duì)于這一塊的理解,就不是隨便在網(wǎng)上找一篇文章學(xué)一下背一下就能達(dá)到要求的了。就要求我們自己有能力去源碼中尋找答案。這篇文章主要是為了幫助大家梳理 diff 算法在源碼中的實(shí)現(xiàn)思路、位置,然后讓我們自己就有能力去總結(jié)出自己的獨(dú)特理解。
一、函數(shù)緩存優(yōu)化
在前端開(kāi)發(fā)中,有一種比較常用的優(yōu)化方式。當(dāng)我們執(zhí)行一個(gè)函數(shù)所需要消耗的時(shí)間偏長(zhǎng)時(shí),第二次執(zhí)行時(shí),我們可以讀取上一次的執(zhí)行結(jié)果,從而跳過(guò)運(yùn)算過(guò)程,直接輸出結(jié)果。
思路大概如下,首先我們定義一個(gè)用于緩存的對(duì)象。
const cache = {
preA: null,
preB: null,
preResult: null
}
這里我們假設(shè) expensive(a, b) 需要花費(fèi)很長(zhǎng)時(shí)間,我們要盡量避免重復(fù)運(yùn)算它。
function cul(a, b) {
return expensive(a, b)
}
于是,我們?cè)诘诙螆?zhí)行 cal 時(shí),就需要提前判斷入?yún)ⅰH绻覀儼l(fā)現(xiàn),兩次的執(zhí)行入?yún)⑹且粯拥模敲次覀兙涂梢圆槐刂匦逻\(yùn)算,而是直接返回上一次的運(yùn)算結(jié)果
代碼調(diào)整如下:
function cul(a, b) {
// 對(duì)比之后選擇復(fù)用緩存的結(jié)果
if (cache.preA === a && cache.preB === b) {
return cache.preResult
}
// 緩存參數(shù)與結(jié)果
cache.preA = a
cache.preB = b
const result = expensive(a, b)
cache.preResult = result
return result
}
那么,當(dāng)我們多次執(zhí)行如下代碼的時(shí)候,耗時(shí)運(yùn)算 expensive() 僅會(huì)執(zhí)行一次。
cul(1000, 999)
cul(1000, 999)
cul(1000, 999)
cul(1000, 999)
cul(1000, 999)
React 的底層更新機(jī)制與這個(gè)簡(jiǎn)化版的優(yōu)化策略一模一樣。只不過(guò) React 遇到的需要緩存的內(nèi)容稍微復(fù)雜一點(diǎn),他的數(shù)據(jù)結(jié)果從一個(gè)普通的 cache 對(duì)象,變成了一個(gè)完整的 Fiber 鏈表。
二、Fiber 鏈表結(jié)構(gòu)
我們常說(shuō)的 Fiber 對(duì)象就是 React 中,用以存儲(chǔ)入?yún)⒑头祷亟Y(jié)果的緩存對(duì)象。這里需要注意的是,和虛擬 DOM 不同,F(xiàn)iber 是一種運(yùn)行時(shí)上下文,他的字段中記錄了大量節(jié)點(diǎn)在運(yùn)行過(guò)程中的狀態(tài)。
function FiberNode(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode) {
// 靜態(tài)數(shù)據(jù)結(jié)構(gòu)
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // 指向真實(shí) DOM 對(duì)象
// 構(gòu)建 Fiber 樹的指針
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 存儲(chǔ)更新與狀態(tài)
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 存儲(chǔ)副作用回調(diào)
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 優(yōu)先級(jí)
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 復(fù)用節(jié)點(diǎn)
this.alternate = null;
}
其中,如下幾個(gè)字段,是構(gòu)成 Fiber 鏈表的重要字段。
// 構(gòu)建 Fiber 樹的指針
this.return = null;
this.child = null;
this.sibling = null;
其中,this.return 指向父節(jié)點(diǎn)。
this.child 指向子元素的第一個(gè)節(jié)點(diǎn)。
this.sibling 指向兄弟節(jié)點(diǎn)。
三、深度有限遍歷
在代碼中,我們的完整應(yīng)用整合起來(lái)大概長(zhǎng)這樣。
<App>
<Header />
<Sider />
<Content />
<Footer />
</App>
當(dāng)然,這只是語(yǔ)法糖,實(shí)際上他的代碼是這樣運(yùn)行的。
function App() {
return (
<>
{Header()}
{Sider()}
{Content()}
{Footer()}
</>
)
}
因此,節(jié)點(diǎn)的執(zhí)行過(guò)程,實(shí)際上就是一個(gè)函數(shù)的正常執(zhí)行過(guò)程。他需要滿足函數(shù)調(diào)用棧的執(zhí)行順序。也就是深度優(yōu)先
四、更新機(jī)制
React 的每一次更新,都是全量更新。因此,他的執(zhí)行,都是從最頂層的根節(jié)點(diǎn)開(kāi)始往下執(zhí)行。這也是 React 被普遍認(rèn)為性能差的核心原因。
?
但是實(shí)際上,充分利用好 React 的 diff 規(guī)則,是可以寫出元素級(jí)別的細(xì)粒度更新的高性能代碼的。只是這對(duì)開(kāi)發(fā)者的要求非常高,很少有開(kāi)發(fā)者能夠充分理解并運(yùn)用 diff 規(guī)則。
因此,當(dāng)我們明白了這種更新機(jī)制之后,在源碼中,就很容易找到每一次更新的起點(diǎn)位置。
五、diff 起點(diǎn)
每一次的更新,都是從根節(jié)點(diǎn)開(kāi)始,該位置在 ReactFiberWorkLoop.js 中。
方法名為 performWorkOnRoot。
?
在之前的版本中,并發(fā)更新的方法名為 performConcurrentWorkOnRoot,同步更新的方法名為 performSyncWorkOnRoot。
export function performWorkOnRoot(
root: FiberRoot,
lanes: Lanes,
forceSync: boolean,
): void {
const shouldTimeSlice =
!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
...
ensureRootIsScheduled(root);
}
其中,renderRootConcurrent 會(huì)啟動(dòng) workLoopConcurrent 循環(huán)。
renderRootSync,該方法會(huì)啟動(dòng) workLoopSync 循環(huán)。
// The fiber we're working on
let workInProgress: Fiber | null = null;
// workInProgress 起始值為根節(jié)點(diǎn)
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
performUnitOfWork(workInProgress);
}
}
workLoopSync 的邏輯非常簡(jiǎn)單,就是開(kāi)始對(duì) Fiber 節(jié)點(diǎn)進(jìn)行遍歷。
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
他們的差別就是是否支持在循環(huán)過(guò)程中,被 shouldYield() 中斷循環(huán)的執(zhí)行。最終他們都會(huì)執(zhí)行 performUnitOfWork。
這里很核心的一個(gè)知識(shí)點(diǎn)就是,workInProgress 是一個(gè)全局上下文變量,他的值會(huì)在執(zhí)行的過(guò)程中不斷發(fā)生變化。許多人會(huì)因?yàn)樵S多文章中提到的雙緩存機(jī)制對(duì)該變量產(chǎn)生誤解。實(shí)際上,他指的是當(dāng)前正在被比較的節(jié)點(diǎn)。而不僅僅只是 Fiber 樹的起點(diǎn)。我們會(huì)在后續(xù)的分析中,看到他不停的被改變,然后執(zhí)行下一個(gè)節(jié)點(diǎn)。
從邏輯中我們可以得出結(jié)論,當(dāng)最終沒(méi)有節(jié)點(diǎn)時(shí),workInProgress = null,循環(huán)才會(huì)結(jié)束。
我們注意關(guān)注接下來(lái)的 performUnitOfWork 方法。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
我們要把 current 與 workInProgress 理解成為一個(gè)一直會(huì)移動(dòng)的指針,他們總是指向當(dāng)前正在執(zhí)行的 Fiber 節(jié)點(diǎn)。當(dāng)前節(jié)點(diǎn)執(zhí)行完之后,我們就會(huì)在修改 workInProgress 的值。
核心的代碼是下面這兩句。
next = beginWork(current, unitOfWork, subtreeRenderLanes);
workInProgress = next;
其中,current 表示已經(jīng)上一次緩存的 Fiber 節(jié)點(diǎn),workInProgress 表示當(dāng)前構(gòu)建的 Fiber 節(jié)點(diǎn)。
了解這個(gè)循環(huán)過(guò)程,是關(guān)鍵。希望我這樣解釋之后,大家都能夠完整的理解到我想要傳達(dá)的含義。
六、beginWork 的作用
這里需要特別注意的是,beginWork 是利用當(dāng)前節(jié)點(diǎn),去計(jì)算下一個(gè)節(jié)點(diǎn)。因此我們要特別關(guān)注他的入?yún)⒑头祷刂?。才能夠更加?zhǔn)確的理解 diff 的原理。
next = beginWork(current, unitOfWork, subtreeRenderLanes);
在 beginWork 的執(zhí)行中,會(huì)優(yōu)先比較當(dāng)前節(jié)點(diǎn)的 props 與 context,來(lái)決定是否需要復(fù)用下一個(gè)節(jié)點(diǎn)。注意理解這句話,可能跟我們常規(guī)的理念很不一樣。這也是準(zhǔn)確理解 React diff 的關(guān)鍵。
我們會(huì)在后續(xù)的代碼中觀察這一邏輯?,F(xiàn)在先來(lái)看一下 beginWork 中的代碼和比較邏輯。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
...
這里的關(guān)鍵是一個(gè)名為 didReceiveUpdate 的全局上下文變量。該變量用于標(biāo)記當(dāng)前 fiber 節(jié)點(diǎn)是否需要復(fù)用其子 fiber 節(jié)點(diǎn)。
其中 props 與 context 的比較代碼如下:
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
...
}
然后這里還有一個(gè)關(guān)鍵比較函數(shù) checkScheduledUpdateOrContext,該函數(shù)用于比較是否存在 update 與 context 的變化。
function checkScheduledUpdateOrContext(
current: Fiber,
renderLanes: Lanes,
): boolean {
// Before performing an early bailout, we must check if there are pending
// updates or context.
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
// No pending update, but because context is propagated lazily, we need
// to check for a context change before we bail out.
if (enableLazyContextPropagation) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
}
return false;
}
這里很難理解的地方在于 state 的比較是如何發(fā)生的。簡(jiǎn)單說(shuō)一下,當(dāng)我們?cè)趧傞_(kāi)始調(diào)用 dispatchReducerAction
等函數(shù)觸發(fā)更新時(shí),都會(huì)提前給被影響的 fiber 節(jié)點(diǎn)標(biāo)記更新優(yōu)先級(jí)。然后再通過(guò) scheduleUpdateOnFiber 進(jìn)入后續(xù)的調(diào)度更新流程。
例如這樣:
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
...
const lane = requestUpdateLane(fiber);
...
scheduleUpdateOnFiber(root, fiber, lane);
因此,我們可以通過(guò) includesSomeLane 方法來(lái)比較前后兩次 fiber 節(jié)點(diǎn)的優(yōu)先級(jí)是否發(fā)生了變化來(lái)判斷是否存在更新。
didReceiveUpdate 的值的變化非常重要,除了在 beginWork 執(zhí)行的時(shí)候,我們比較了 props 和 context 之外,在前置的邏輯中,還設(shè)定一個(gè)了一個(gè)方法用于設(shè)置他的值為 true。
export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}
該方法被運(yùn)用在 state 的比較結(jié)果中。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
...
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
...
}
當(dāng) state、props、context 的比較結(jié)果都沒(méi)有發(fā)生變化時(shí),表示此時(shí)沒(méi)有更新發(fā)生,因此會(huì)直接進(jìn)入 bailout。
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
后續(xù)調(diào)用的方法為 bailoutOnAlreadyFinishedWork,會(huì)返回當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)進(jìn)行復(fù)用,重點(diǎn)關(guān)注下面的代碼。
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
如果比較結(jié)果是無(wú)法復(fù)用,那么就會(huì)根據(jù)不同的 tag 執(zhí)行不同的創(chuàng)建函數(shù)。
switch (workInProgress.tag) {
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
disableDefaultPropsExceptForClasses ||
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
workInProgress.elementType === Component,
);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
...
}
我們重點(diǎn)關(guān)注 updateFunctionComponent,并重點(diǎn)關(guān)注如下幾行代碼。
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
reconcileChildren 中會(huì)調(diào)用 reconcileChildFibers 方法,該方法則可以被稱為是子節(jié)點(diǎn) diff 的入口函數(shù)。他會(huì)根據(jù) newChild 的不同類型做不同的處理。
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}
子節(jié)點(diǎn)的類型非常多,每一種類型如何處理我們都要單獨(dú)去判斷。我們這里以其中一個(gè)單元素節(jié)點(diǎn)為例 reconcileSingleElement 來(lái)繼續(xù)分析。他的代碼如下:
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
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) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
} else {
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
在代碼中我們可以看到,這里會(huì)以此比較 key、type、tag 的值。如果都相同,則選擇復(fù)用,返回已經(jīng)存在的節(jié)點(diǎn)。
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
到這里,diff 的整個(gè)流程都已經(jīng)比較清楚了。在理解 diff 時(shí),我們對(duì) Fiber 的鏈表結(jié)構(gòu)足夠清晰,明確兩個(gè)指針 current 與 workInProgress 的含義與移動(dòng)方向,理解起來(lái)就會(huì)非常容易。
七、總結(jié)
由于時(shí)間有限,文字的表達(dá)能力有限,本文并沒(méi)有完全的從結(jié)論的角度為大家介紹 diff 算法是如何如何。而是從源碼的角度引導(dǎo)大家應(yīng)該如何在代碼中去自己提煉相關(guān)的觀點(diǎn)。因此,主要的表訴方式是以大概的實(shí)現(xiàn)思路加上源碼的函數(shù)調(diào)用路徑來(lái)跟大家分享。