03 - React 中的更新
React 中創(chuàng)建更新的方式:
初次渲染:ReactDOM.render纲酗、ReactDOM.hydrate
后續(xù)更新:setState衰腌、forceUpdate
1. ReactDOM.render()
- 先創(chuàng)建 ReactRoot 頂點(diǎn)對(duì)象
- 然后創(chuàng)建 FiberRoot 和 RootFiber
- 創(chuàng)建更新,使應(yīng)用進(jìn)入更新調(diào)度過程
這個(gè)部分觅赊,只要了解流程即可右蕊,不要陷入各種旁支末節(jié),否則很難再 “return”出來吮螺,劃不來饶囚,先點(diǎn)到為止帕翻。
寫 JSX 的時(shí)候,只是調(diào)用了 createElement 創(chuàng)建了 element 樹萝风,還需要 render 進(jìn)一步進(jìn)行渲染和處理熊咽。
ReactDOM 源碼在 react-dom/src/client 下面,而 server 對(duì)應(yīng)的是服務(wù)端闹丐,這里只研究客戶端横殴。
const ReactDOM: Object = {
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
return legacyRenderSubtreeIntoContainer(
null, // 沒有父組件
element,
container,
false, // 不調(diào)和
callback,
);
},
// hydrate 和 render 唯一區(qū)別就是是否會(huì)調(diào)和 DOM 節(jié)點(diǎn),是否會(huì)復(fù)用節(jié)點(diǎn)卿拴,服務(wù)端的時(shí)候會(huì)用到衫仑,暫且不表
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
},
// ... 其他方法略
}
渲染子樹到 container 中:
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
let root: Root = (container._reactRootContainer: any);
if (!root) {
// Initial mount 首次掛載時(shí) container 上自然沒有綁定過 _reactRootContainer
// 接著就是根據(jù)傳入的 container 創(chuàng)建 ReactRoot 并順便綁定到 container 上,
// 這個(gè) ReactRoot 對(duì)象中的 _internalRoot 是一個(gè) FiberRoot。
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
// 首次渲染不需要所謂的批量更新
DOMRenderer.unbatchedUpdates(() => {
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
// 一般來說 parentComponnent 就是 null堕花,所以會(huì)走到這里提交更新
root.render(children, callback); // 具體見后面的代碼塊
}
});
} else {
// 下次更新文狱,除了不再放入 DOMRenderer.unbatchedUpdates 回調(diào)中執(zhí)行,其他和首次渲染一樣
// 略
}
return DOMRenderer.getPublicRootInstance(root._internalRoot);
}
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): Root {
// 內(nèi)部通過判斷傳入的 root 節(jié)點(diǎn)是否有子節(jié)點(diǎn)來決定是否進(jìn)行調(diào)和缘挽。
// 非服務(wù)端的話瞄崇,不涉及 hydrate 調(diào)和,接下來就是清空傳入的 root dom 下面的子節(jié)點(diǎn)壕曼,因?yàn)榻觼硐?react 要掛載自己的 dom 到 root 上苏研。
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
// 清空 container dom 下的子節(jié)點(diǎn)
if (!shouldHydrate) {
let warned = false;
let rootSibling;
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
// Legacy roots are not async by default.
const isConcurrent = false;
//
return new ReactRoot(container, isConcurrent, shouldHydrate);
}
ReactRoot 是外層包裹,里面的 _internalRoot
才是 FiberRoot:
function ReactRoot(
container: Container,
isConcurrent: boolean,
hydrate: boolean,
) {
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}
react-reconciler 包下腮郊,創(chuàng)建 FiberRoot:
export function createContainer(
containerInfo: Container,
isConcurrent: boolean,
hydrate: boolean,
): OpaqueRoot {
return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
在前面的 legacyRenderSubtreeIntoContainer 中的 root.render(children, callback):
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (callback !== null) {
work.then(callback);
}
DOMRenderer.updateContainer(children, root, null, work._onCommit);
return work;
};
DOMRenderer.updateContainer 內(nèi)部的深層調(diào)用摹蘑。createUpdate() 創(chuàng)建 update 對(duì)象,把要更新的 element 添加到 update 上轧飞,然后 update 進(jìn)入更新隊(duì)列衅鹿,然后開始調(diào)度更新的工作:
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function,
) {
const update = createUpdate(expirationTime);
// Caution: React DevTools currently depends on this property
// being called "element".
// 被調(diào)用的 element 作為 update 對(duì)象的載荷
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
warningWithoutStack(
typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callback,
);
update.callback = callback;
}
// 把 update 加入更新隊(duì)列
enqueueUpdate(current, update);
// 開始調(diào)用更新
scheduleWork(current, expirationTime);
return expirationTime;
}
在 ReactRoot 中會(huì)創(chuàng)建 FiberRoot 然后賦值到 this._internalRoot
,this 就是指 ReactRoot 實(shí)例过咬。然后順便把內(nèi)部創(chuàng)建出來的 ReactRoot 對(duì)象綁定到最初傳入的 root dom 節(jié)點(diǎn)(通常是個(gè)div)的 _reactRootContainer
屬性上大渤,見下圖:
在 React 17中 ReactDOM.render() 不再能夠用來 hydrate 調(diào)和服務(wù)端渲染的 container,會(huì)被廢棄掸绞。有此需求應(yīng)直接使用 ReactDOM.hydrate()
泵三。
Using ReactDOM.render() to hydrate a server-rendered container is deprecated and will be removed in React 17. Use hydrate() instead.
2. FiberRoot
Fiber 解決了單線程計(jì)算量過大時(shí)交互、動(dòng)畫卡頓的問題集漾,常說的 “虛擬DOM” 就是指 Fiber 樹切黔。
FiberRoot:
- 整個(gè)應(yīng)用的起點(diǎn)
- 包含應(yīng)用掛載的目標(biāo)節(jié)點(diǎn)
- 記錄整個(gè)應(yīng)用更新過程的各種信息
React.createElement() 創(chuàng)建出 ReactElement 節(jié)點(diǎn),組成 element 樹具篇, 每一個(gè)的 element 也都有對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)纬霞,組成 Fiber 樹。
**FiberRoot **中的一些屬性:
- containerInfo: root 節(jié)點(diǎn)驱显,即 ReactDOM.render() 方法接收到的第二個(gè)參數(shù)诗芜。
- current:記錄了當(dāng)前入口 dom 節(jié)點(diǎn)所對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)瞳抓,即 RootFiber(涉及到雙緩存/雙buff/double-buff 機(jī)制)。
- finishedWork:一次更新渲染過程中完成了的那個(gè)更新任務(wù)伏恐。更新完成之后讀取該屬性孩哑,渲染至 dom 上。
- nextExpirationTimeToWorkOn:下次更新時(shí)要執(zhí)行的那個(gè)任務(wù)翠桦,react 會(huì)遍歷 fiber 樹横蜒,讀取每個(gè) fiber 節(jié)點(diǎn)上的 ExpirationTIme,在 FiberRoot 上用該屬性記錄最高優(yōu)先級(jí)的那個(gè)任務(wù)销凑。
- expirationTime: 當(dāng)前更新對(duì)應(yīng)的過期時(shí)間丛晌。
- nextScheduledRoot 存在多個(gè) root 掛載點(diǎn)時(shí),會(huì)有多個(gè) FiberRoot斗幼,而這些 FiberRoot 會(huì)組成單向鏈表澎蛛,因此該屬性就是指向鏈表中下一個(gè) root 節(jié)點(diǎn)的“指針”。這個(gè)屬性蜕窿,也體現(xiàn)了谋逻,為什么在入口 dom 節(jié)點(diǎn)所對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)上,還需要一層結(jié)構(gòu)桐经,即 FiberRoot毁兆。
3. Fiber
- 每一個(gè) ReactElement 對(duì)應(yīng)一個(gè) Fiber 對(duì)象。
- Fiber 對(duì)象上記錄了節(jié)點(diǎn)的各種狀態(tài)次询,包括 state 和 props荧恍。Fiber 更新完成之后瓷叫,state 和 props 才被更新到 class 組件的 this 上屯吊,也為 hooks 的實(shí)現(xiàn)提供了根基,因?yàn)闋顟B(tài)并不是靠 function 函數(shù)本身來維持的摹菠。
- 串聯(lián)整個(gè)應(yīng)用形成樹結(jié)構(gòu)盒卸。
Fiber 樹遍歷時(shí)根據(jù) child、sibling次氨、return(parent)蔽介,Fiber 部分屬性如下:
- tag: 標(biāo)記不同的組件類型
- elementType: ReactElement.type,也就是我們調(diào)用
createElement()
的第一個(gè)參數(shù)煮寡。 - stateNode:記錄組件實(shí)例虹蓄,如 class 組件的實(shí)例、原生 dom 實(shí)例幸撕,而 function 組件沒有實(shí)例薇组,因此該屬性是空。如 state坐儿、props 等狀態(tài)完成更新任務(wù)后律胀,react 會(huì)通過該屬性宋光,更新組件實(shí)例。需要強(qiáng)調(diào)的是炭菌,RootFiber 的
stateNode
屬性指向 FiberRoot罪佳,和 FiberRoot 上的current
屬性相呼應(yīng)。 - penndingProps: 新的 props
- memorizedProps: 老的 props(上次渲染完成之后的 props)
- memorizedState:老的 state(上次渲染完成之后的 state)
- updateQueue: 該 Fiber 對(duì)應(yīng)的組件產(chǎn)生的 update 會(huì)存放于該隊(duì)列(類似于單向鏈表)中黑低。該過程產(chǎn)出的新的 state 會(huì)用來更新 memorizedState赘艳。
- expirationTime: 代表任務(wù)在未來的哪個(gè)時(shí)間點(diǎn)應(yīng)該被完成,不包括他的子樹產(chǎn)生的任務(wù)克握。
- childExpirationTime: 子樹中優(yōu)先級(jí)最高的過期時(shí)間第练,即最先的過期時(shí)間,用于快速確定子樹中是否有不在等待的變化玛荞。
- alternate: 在 Fiber 樹每次更新時(shí)娇掏,每個(gè) FIber 都會(huì)有一個(gè)與其對(duì)應(yīng)的 Fiber,稱為“current <--> workInProgress”勋眯。React 應(yīng)用的根節(jié)點(diǎn)(FiberRoot)通過 current 指針在不同 Fiber 樹間進(jìn)行切換毒涧,從而兩個(gè) Fiber 樹輪流復(fù)用(雙緩存機(jī)制)检痰,而不是每次更新都創(chuàng)建新的 Fiber 樹。其中很多 workInProgress fiber 的創(chuàng)建可以復(fù)用 current Fiber 樹對(duì)應(yīng)的節(jié)點(diǎn)數(shù)據(jù)(因?yàn)槊總€(gè) Fiber 節(jié)點(diǎn)都有 alternate 指向?qū)?yīng)的節(jié)點(diǎn)),這個(gè)決定是否復(fù)用 current Fiber 樹對(duì)應(yīng)節(jié)點(diǎn)數(shù)據(jù)的過程就是 Diff 算法睬罗。
- mode: 用來描述當(dāng)前 Fiber 和其子樹的模式(后面會(huì)提到)
// Effect 系列
- effectTag: SideEffectTag。用來記錄 SideEffect休建。
- nextEffect: Fiber | null袍镀。單鏈表用來快速查找下一個(gè)side effect。
- firstEffect: Fiber | null辆琅。 子樹中第一個(gè)side effect漱办。
- lastEffect: Fiber | null。子樹中最后一個(gè)side effect婉烟。
TODO: 補(bǔ)一張 Fiber 樹圖娩井。
Fiber.tag
export type WorkTag =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18;
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
4. update 和 updateQueue
Update:
- 用于記錄組件狀態(tài)的改變
- 存放于 UpdateQueue 中
- 多個(gè) Update 可以同時(shí)存在(因?yàn)榉旁陉?duì)列中)
export type Update<State> = {
// 更新的過期時(shí)間
expirationTime: ExpirationTime,
// export const UpdateState = 0;
// export const ReplaceState = 1;
// export const ForceUpdate = 2;
// export const CaptureUpdate = 3;
// 指定更新的類型,值為以上幾種
tag: 0 | 1 | 2 | 3,
// 更新內(nèi)容似袁,比如`setState`接收的第一個(gè)參數(shù)
payload: any,
// 對(duì)應(yīng)的回調(diào)洞辣,`setState`,`render`都有
callback: (() => mixed) | null,
// 指向下一個(gè)更新
next: Update<State> | null,
// 指向下一個(gè)`side effect`
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
// 每次操作完更新之后的`state`
baseState: State,
// 隊(duì)列中的第一個(gè)`Update`
firstUpdate: Update<State> | null,
// 隊(duì)列中的最后一個(gè)`Update`
lastUpdate: Update<State> | null,
// 第一個(gè)捕獲類型的`Update`
firstCapturedUpdate: Update<State> | null,
// 最后一個(gè)捕獲類型的`Update`
lastCapturedUpdate: Update<State> | null,
// 第一個(gè)`side effect`
firstEffect: Update<State> | null,
// 最后一個(gè)`side effect`
lastEffect: Update<State> | null,
// 第一個(gè)和最后一個(gè)捕獲產(chǎn)生的`side effect`
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | null,
};
Update:
- expirationTime: 更新的過期時(shí)間昙衅。
- payload:首次渲染 payload 是整個(gè) element 樹扬霜,而后續(xù)如 setState 觸發(fā)更新,則 payload 是 setState 傳入的參數(shù)而涉,即 state 對(duì)象或者函數(shù)著瓶。
- tag: 0 | 1 | 2 | 3 指定更新的類型,值為:UpdateState | ReplaceState | ForceUpdate | CaptureUpdate)
- callback: 對(duì)應(yīng)的回調(diào)婴谱,
setState
或者render
都有蟹但。 - next: 指向下一個(gè)更新躯泰。
- nextEffect: 指向下一個(gè) side effect。
UpdateQueue:
- baseState:每次操作完更新之后的 state华糖,作為下次更新 state 時(shí)的計(jì)算依據(jù)麦向。
- firstUpdate: 更新隊(duì)列中第一個(gè) Update
- lastUpdate: 更新隊(duì)列中最后一個(gè) Update
- firstCapturedUpdate
- lastCapturedUpdate
- firstEffect
- lastEffect
- firstCapturedEffect
- lastCapturedEffect
上面說過 ReatDOM.render() 時(shí)創(chuàng)建 Update 并添加到 UpdateQueue 中。enqueueUpdate() (位于 react-reconciler/ReactUpdateQueue.js)用于初始化 Fiber 對(duì)象上的 updateQueue客叉,以及如果已經(jīng)存在時(shí)則更新這個(gè)隊(duì)列诵竭。在此過程中,保持雙 Fiber 的 updateQueue 的首尾 queue 一致兼搏。
enqueueUpdate()
:
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternate;
let queue1;
let queue2;
// 創(chuàng)建或更新隊(duì)列卵慰,若兩個(gè)隊(duì)列都不存在,則各自創(chuàng)建一個(gè)隊(duì)列佛呻;
// 若其中一個(gè)隊(duì)列存在時(shí)裳朋,則 clone 出另一個(gè)隊(duì)列,會(huì)共享三個(gè)屬性:baseState吓著、firstUpdate鲤嫡、lastUpdate
if (alternate === null) {
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// There are two owners.
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// Neither fiber has an update queue. Create new ones.
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(
alternate.memoizedState,
);
} else {
// Only one fiber has an update queue. Clone to create a new one.
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// Both owners have an update queue.
}
}
}
// 2. 調(diào)用 appendUpdateToQueue() 將 update 添加到隊(duì)列鏈表中
if (queue2 === null || queue1 === queue2) {
// There's only a single queue.
appendUpdateToQueue(queue1, update);
} else {
// There are two queues. We need to append the update to both queues,
// while accounting for the persistent structure of the list — we don't
// want the same update to be added multiple times.
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// One of the queues is not empty. We must add the update to both queues.
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// Both queues are non-empty. The last update is the same in both lists,
// because of structural sharing. So, only append to one of the lists.
appendUpdateToQueue(queue1, update);
// But we still need to update the `lastUpdate` pointer of queue2.
queue2.lastUpdate = update;
}
}
}
appendUpdateToQueue()
,UpdateQueue 顯然是一個(gè)基于鏈表的隊(duì)列绑莺,看情況更新首尾指針即可:
function appendUpdateToQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>,
) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
cloneUpdateQueue
:
function cloneUpdateQueue<State>(
currentQueue: UpdateQueue<State>,
): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
// 克隆隊(duì)列時(shí)這三個(gè)屬性是共享的
baseState: currentQueue.baseState,
firstUpdate: currentQueue.firstUpdate,
lastUpdate: currentQueue.lastUpdate,
// TODO: With resuming, if we bail out and resuse the child tree, we should
// keep these effects.
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}
5. ExpirationTime
尤其對(duì)于異步任務(wù)來說暖眼,過期時(shí)間是某個(gè)更新任務(wù)告訴 react 在過期時(shí)間未到之前,自己可以被打斷纺裁。但如果過期時(shí)間已經(jīng)到了诫肠,而更新任務(wù)依舊未得到執(zhí)行,則會(huì)被強(qiáng)制執(zhí)行欺缘。
- currentTime:簡單理解當(dāng)前時(shí)間距 JS 加載完成時(shí)的時(shí)間
- 在一次渲染中產(chǎn)生的更新需要使用相同的時(shí)間
- 一次批處理的更新應(yīng)該得到相同的時(shí)間
- 掛起任務(wù)用于記錄的時(shí)候應(yīng)該相同
- expirationTime:過期時(shí)間
react-reconciler/ReactFiberReconciler.js
updateContainer()
:
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
const current = container.current;
// 獲取 currentTime
const currentTime = requestCurrentTime();
// 根據(jù) currentTime 計(jì)算過期時(shí)間(其實(shí)并不是直接計(jì)算栋豫,而是先調(diào)用)
const expirationTime = computeExpirationForFiber(currentTime, current);
// 然后就是上面剛剛說過的創(chuàng)建 update 和 updateQueue 的過程
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}
requestCurrentTime()
:
同一事件中的兩個(gè)更新計(jì)劃應(yīng)被處理為同時(shí)發(fā)生,即使它們的時(shí)鐘時(shí)間必然有先有后浪南。因?yàn)?expirationTime 決定了如何處理批量更新笼才,所以這里出于性能考慮,在同一事件中络凿,類似優(yōu)先級(jí)的更新任務(wù)會(huì)得到相同的 currentTime,從而后面計(jì)算出相同的 expirationTime昂羡,這些任務(wù)在某一時(shí)刻同時(shí)更新絮记,避免短期內(nèi)多次頻繁更新崩潰:
function requestCurrentTime() {
// requestCurrentTime is called by the scheduler to compute an expiration
// time.
//
// Expiration times are computed by adding to the current time (the start
// time). However, if two updates are scheduled within the same event, we
// should treat their start times as simultaneous, even if the actual clock
// time has advanced between the first and second call.
// In other words, because expiration times determine how updates are batched,
// we want all updates of like priority that occur within the same event to
// receive the same expiration time. Otherwise we get tearing.
//
// We keep track of two separate times: the current "renderer" time and the
// current "scheduler" time. The renderer time can be updated whenever; it
// only exists to minimize the calls performance.now.
//
// But the scheduler time can only be updated if there's no pending work, or
// if we know for certain that we're not in the middle of an event.
if (isRendering) {
// We're already rendering. Return the most recently read time.
return currentSchedulerTime;
}
// Check if there's pending work.
findHighestPriorityRoot();
if (
nextFlushedExpirationTime === NoWork ||
nextFlushedExpirationTime === Never
) {
// If there's no pending work, or if the pending work is offscreen, we can
// read the current time without risk of tearing.
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
return currentSchedulerTime;
}
// There's already pending work. We might be in the middle of a browser
// event. If we were to read the current time, it could cause multiple updates
// within the same event to receive different expiration times, leading to
// tearing. Return the last read time. During the next idle callback, the
// time will be updated.
return currentSchedulerTime;
}
computeExpirationForFiber()
方法更多信息下一小節(jié)再說,其中涉及到的 expirationTime 計(jì)算過程:
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
export type ExpirationTime = number;
export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;
// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
//
// If the main thread is being blocked so long that you hit the expiration,
// it's a problem that could be solved with better scheduling.
//
// People will be more likely to notice this and fix it with the long
// expiration time in development.
//
// In production we opt for better UX at the risk of masking scheduling
// problems, by expiring fast.
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
過期時(shí)間 = 當(dāng)前時(shí)間 + 延遲
延遲的時(shí)間長度如下(ms):
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150; // 高優(yōu)先級(jí)任務(wù)的過期時(shí)間基礎(chǔ)偏移量
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export const LOW_PRIORITY_EXPIRATION = 5000; // 高優(yōu)先級(jí)任務(wù)的過期時(shí)間基礎(chǔ)偏移量
export const LOW_PRIORITY_BATCH_SIZE = 250;
最終計(jì)算出的 expirationTime 的精度是 10ms(高優(yōu)先級(jí)) 或者 25ms(低優(yōu)先級(jí))虐先,即 expirationTime 會(huì)是 10 或者 25 的整數(shù)倍怨愤。
bucketSIzeMs / UNIT_SIZE 精度在這里的意義:
如果在一個(gè)操作內(nèi)多次調(diào)用了 setState,即便前后調(diào)用的時(shí)間差距可能很小蛹批,但毫秒級(jí)別還是有差距撰洗,那么計(jì)算出的 expirationTime 也就不一樣篮愉,任務(wù)優(yōu)先級(jí)也就不一樣,導(dǎo)致 react 更新多次差导,導(dǎo)致整個(gè)應(yīng)用性能下降试躏。
而有了 精度/粒度 的控制,使得非常詳盡的兩次更新设褐,即使具有微小的 currentTime 差異颠蕴,也會(huì)得到相同的 expirationTime,從而到時(shí)候在一次更新中一起完成(批量更新)助析。
currentTime 和 expirationTime 在各自計(jì)算過程中犀被,為了性能都在**保證在一個(gè)批量更新中產(chǎn)生的同類型的更新,應(yīng)具有相同的過期時(shí)間外冀。 **否則全部用當(dāng)前時(shí)間加上固定的延遲作為未來的過期時(shí)間就用不著計(jì)算這么麻煩了寡键。
6. 不同的 ExpirationTime
-
NoWork
: 代表沒有更新 -
Sync
: 代表同步執(zhí)行,不會(huì)被調(diào)度也不會(huì)被打斷 -
async
: 異步模式下計(jì)算出來的過期時(shí)間雪隧,一個(gè)時(shí)間戳昌腰,會(huì)被調(diào)度,同時(shí)還可能被打斷
export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;
上一小節(jié)中提到膀跌,得到 currentTime 后遭商,會(huì)調(diào)用 computeExpirationForFiber()
然后返回 expirationTime,還涉及到過期時(shí)間的復(fù)雜的計(jì)算公式捅伤,但有些過期時(shí)間的計(jì)算其實(shí)不需要調(diào)用計(jì)算公式:
比如后面提到的 flushSync
中把 expirationContext 改為 Sync劫流,直接進(jìn)入下面第一個(gè)條件判斷,最終得到的過期時(shí)間直接就是 Sync丛忆,也就是 0ms:
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) {
// An explicit expiration context was set;
expirationTime = expirationContext;
} else if (isWorking) {
if (isCommitting) {
// Updates that occur during the commit phase should have sync priority
// by default.
expirationTime = Sync;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.
expirationTime = nextRenderExpirationTime;
}
} else {
// No explicit expiration context was set, and we're not currently
// performing work. Calculate a new expiration time.
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
expirationTime = computeAsyncExpiration(currentTime);
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime += 1;
}
} else {
// This is a sync update
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
- 通過外部來強(qiáng)制某一個(gè)更新必須使用哪一種 expirationTime(指定 expirationContext):
比如使用 ReactDOM.flushSync()
(該方法實(shí)際存在于 react-reconciler/ReactFiberScheduler.js 中) 可以指定 expirationTime 為 1ms祠汇,意味著同步更新:
import { flushSync } from 'react-dom';
// ...
handleClick = () => {
flushSync(() => {
this.setState({ text: '666' });
});
};
// ...
let expirationContext: ExpirationTime = NoWork;
// ...
function flushSync<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return syncUpdates(fn, a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
performSyncWork();
}
}
function syncUpdates<A, B, C0, D, R>(
fn: (A, B, C0, D) => R,
a: A,
b: B,
c: C0,
d: D,
): R {
const previousExpirationContext = expirationContext;
expirationContext = Sync; // 設(shè)置為 1
try {
return fn(a, b, c, d); // 傳入 flushSync 的回調(diào)函數(shù)在這里被執(zhí)行
} finally {
expirationContext = previousExpirationContext; // 把 expirationContext 恢復(fù)成 NoWork
}
}
- isWorking/isCommitting,即有任務(wù)更新的時(shí)候:
同樣也不需要什么計(jì)算公式熄诡。具體留待后面涉及更新的時(shí)候再說可很。
- 處于 ConcurrentMode 模式下才需要異步更新,即需要用到計(jì)算公式:
對(duì)于大部分的 react 事件系統(tǒng)產(chǎn)生的更新凰浮,這里的 isBatchingInteractiveUpdates
會(huì)是 true 我抠,也就是高優(yōu)先級(jí)的任務(wù),過期時(shí)間會(huì)更短袜茧。
if (fiber.mode & ConcurrentMode) {
// 大部分的 react 事件產(chǎn)生的更新中 isBatchingInteractiveUpdates 會(huì)是 true 菜拓,
// 也就是高優(yōu)先級(jí)的任務(wù),過期時(shí)間會(huì)更短笛厦。
if (isBatchingInteractiveUpdates) {
expirationTime = computeInteractiveExpiration(currentTime);
} else {
expirationTime = computeAsyncExpiration(currentTime);
}
// 正在渲染樹時(shí)纳鼎,新加入的更新的過期時(shí)間+1 以遍不會(huì)和當(dāng)前更新一起更新。后續(xù)講更新時(shí)再細(xì)說
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime += 1;
}
} else {
expirationTime = Sync;
}
至于 fiber.mode & ConcurrentMode 這種按位操作的表達(dá)式,其實(shí)就是使用位運(yùn)算進(jìn)行屬性的讀寫贱鄙。
使用一個(gè)若干位的二進(jìn)制數(shù)表達(dá)(存儲(chǔ))若干個(gè)布爾屬性劝贸,設(shè)置屬性使用按位異或 ^
,查詢屬性使用按位與 &
逗宁。
Fiber 上的 mode:
export type TypeOfMode = number;
export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;
7. setState 和 forceUpdate
在 react 中能合理產(chǎn)生更新的方式映九,同時(shí)也是 react 推崇的方式有以下幾種:
- ReactDOM.render() 首次渲染
- setState(class 組件)
- forceUpdate (class 組件)這個(gè)其實(shí)也很少使用
- useState (函數(shù)式組件中的 hooks)
ReactDOM.render 創(chuàng)建的更新是放在 RootFiber 上面,是整體的初始化渲染疙剑。
是針對(duì)setState 和 forceUpdate 是為節(jié)點(diǎn)的 Fiber 創(chuàng)建更新氯迂,是針對(duì)某一個(gè) class component 而言。
和之前的 ReactDOM.render()
內(nèi)部會(huì)調(diào)用的 updateContaine()
方法很像言缤,在 enqueueSetState()
和 enqueueForceUpdate()
中:
- 拿到 fiber嚼蚀,得到 currentTime,一起作為
computeExpirationForFibe()
的參數(shù)算出 expirationTime管挟。 - 然后創(chuàng)建 update 對(duì)象轿曙,然后
enqueueUpdate()
時(shí)看情況創(chuàng)建或更新隊(duì)列,然后進(jìn)行調(diào)度僻孝。
如之前說的导帝,update 對(duì)象上的 payload 載荷在 ReactDOM.render 時(shí)是 element 樹,而在 setState 或 forceUpdate 時(shí)是傳入的新的 state 對(duì)象(可能是局部的 state 對(duì)象)穿铆。
forceUpdate 和 setState 進(jìn)行 enqueue 時(shí)唯一不同點(diǎn)在于 forceUpdate 所創(chuàng)建的 update 對(duì)象上的 tag 會(huì)是 ForceUpdate
而不是默認(rèn)的 UpdateState
您单。
react-reconciler/src/ReactFiberClassComponent.js 中:
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ReplaceState;
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueForceUpdate(inst, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
};
可見,在 react 中創(chuàng)建更新的過程基本一樣荞雏,虐秦。而更多的技術(shù)細(xì)節(jié)會(huì)在整體的 Scheduler 調(diào)度方面。
ReactDOM.render/setState/forceUpdate 最終都會(huì)創(chuàng)建 update 對(duì)象凤优,掛載 payload 載荷悦陋,并添加到各自 Fiber 節(jié)點(diǎn)上的 updateQueue 中,然后即將進(jìn)入下一環(huán)節(jié)筑辨,開始 scheduleWork 即調(diào)度工作俺驶。
下一篇就來分析創(chuàng)建更新隊(duì)列之后,react 如何進(jìn)行統(tǒng)一調(diào)度棍辕。