回顧 React 更新
創(chuàng)建更新
ReactDOM.render 初始渲染
每次調(diào)用都通過傳入的<App />, getElementById('app')
構(gòu)建 root 節(jié)點亿鲜,每個 rootFiber 都有獨立的 updateQueue 和 fiberTree芽世,最后調(diào)用 ReactRoot.prototypye.render 來創(chuàng)建更新容客。setState & forceUpdate 更新渲染
都是 Component 構(gòu)造函數(shù)的原型方法剥哑,目的都是給節(jié)點的 fiber 對象上創(chuàng)建更新,區(qū)別在于更新的類型不同铃慷。
創(chuàng)建更新 update考抄,記錄當(dāng)前時間,計算 expirationTime沐绒,設(shè)置當(dāng)前更新的 payload俩莽,再把 update 推入 fiber 對象的 updateQueue 屬性上, 之后進(jìn)入調(diào)度流程。
expirationTime
由 ReactFiberReconciler.js 包乔遮,updateContainer 中的 const expirationTime = computeExpirationForFiber(currentTime, current);
計算出
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
// ...
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update 高優(yōu)先級
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update 低優(yōu)先級
expirationTime = computeAsyncExpiration(currentTime);
}
} else {
// This is a sync update
expirationTime = Sync;
}
}
- 簡化 computeExpirationForFiber 函數(shù)
- 發(fā)現(xiàn) expirationTime 正常情況是 Sync = 1 同步的
- 只有在 fiber.mode 存在并且使用 ConcurrentMode 新版本的異步更新模式時才會真正的計算 expirationTime
- ConcurrentMode 模式下還會根據(jù) isBatchingInteractiveUpdates 全局變量判斷當(dāng)前更新的上下文環(huán)境來決定 expirationTime 是高優(yōu)先級還是低優(yōu)先級的運(yùn)算結(jié)果扮超。(isBatchingInteractiveUpdates 在 batchedUpdates 中講解)
scheduleWork 開始調(diào)度
核心功能
- 找到更新對應(yīng)的 FiberRoot 節(jié)點
setState 時傳入的都是組件的 Fiber 節(jié)點而不是 FiberRoot 節(jié)點 - 符合條件時 - 重置 stack
具有公共變量,用于調(diào)度和更新 - 符合條件時 - 請求工作調(diào)度
回顧 FiberTree
FiberTree 屬性
1 child 為第一個子節(jié)點
2 sibling 為兄弟節(jié)點
3 return 為父節(jié)點,只有 RootFiber 對象 renturn 為 null
4 FiberRoot.current 和 RootFiber.stateNode 互相引用執(zhí)行操作時的 Fiber 對象
1 點擊組件上的元素
2 執(zhí)行組件的原型方法調(diào)用 setState
3 把 RootFiber 加入到調(diào)度中
scheduleWork 進(jìn)入調(diào)度隊列
每一次進(jìn)入調(diào)度隊列的只有 FiberRoot 對象, 更新也是從 FiberRoot 對象上開始的出刷。
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
// 找到 root 更新 FiberTree 上的所有 expirationTime
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) { // 沒有 FiberRoot 暫停
return;
}
if (
!isWorking && // 沒有執(zhí)行渲染
nextRenderExpirationTime !== NoWork && // 任務(wù)是個異步的璧疗,執(zhí)行到一半了,交還給瀏覽器執(zhí)行
expirationTime < nextRenderExpirationTime // 新的任務(wù)優(yōu)先級高于現(xiàn)在的任務(wù)
) {
// This is an interruption. (Used for performance tracking.)
interruptedBy = fiber; // 記錄
resetStack(); // 優(yōu)先執(zhí)行高優(yōu)先級任務(wù)
}
markPendingPriorityLevel(root, expirationTime);
if (
!isWorking || // 沒有正在工作
isCommitting || // 或者正在提交馁龟,也就是更新dom 樹的渲染階段
nextRoot !== root // 不同的 root 一般不存在不同
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime); // 請求工作
}
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// Reset this back to zero so subsequent updates don't throw.
nestedUpdateCount = 0;
invariant( false, 超出最大更新深度崩侠。 當(dāng)組件在componentWillUpdate或componentDidUpdate中重復(fù)調(diào)用setState時,可能會發(fā)生這種情況屁柏。 React限制嵌套更新的數(shù)量以防止無限循環(huán)啦膜。 );
}
}
scheduleWorkToRoot 通過 Fiber 對象找到 RootFiber 對象進(jìn)行調(diào)度
- 根據(jù)傳入的 Fiber 對象向上尋找到 RootFiber 對象
- 同時更新所有子樹上面的 expirationTime
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
// ...
if ( // 更新 fiber 對象上 expirationTime
fiber.expirationTime === NoWork || // 沒有任何更新操作的
fiber.expirationTime > expirationTime // 有更新產(chǎn)生,但是優(yōu)先級低于新計算的 expirationTime
) { // 設(shè)置成最新的 expirationTime
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
if (
alternate !== null &&
(alternate.expirationTime === NoWork ||
alternate.expirationTime > expirationTime)
) {
// 邏輯和上面一樣淌喻,更新 alternate 的expirationTime
alternate.expirationTime = expirationTime;
}
// 通過 FiberTree 的屬性向上尋找 FiberRoot 并更新每個子 fiber 對象的 expirationTime
let node = fiber.return; // renturn 父節(jié)點僧家,
let root = null;
// node === null 就是 FiberRoot 對象
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode;
} else {
// 循環(huán)查找 FiberRoot
while (node !== null) {
alternate = node.alternate;
if ( // 更新 expirationTime
node.childExpirationTime === NoWork ||
node.childExpirationTime > expirationTime
) {
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
} else if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
// 找到 FiberRoot 結(jié)束循環(huán)
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
// 繼續(xù)向父節(jié)點查找
node = node.return;
}
}
resetStack
- 當(dāng)發(fā)現(xiàn)當(dāng)前任務(wù)的優(yōu)先級大于下一個任務(wù)的優(yōu)先級時,把下個任務(wù)的優(yōu)先級重置執(zhí)行當(dāng)前任務(wù)
- resetStack 在重置下個任務(wù)時裸删,會先記錄這個任務(wù)八拱,等待以后執(zhí)行,并且使用 unwindInterruptedWork 來重置這個任務(wù) fiber 上級的狀態(tài)
if (
!isWorking && // 沒有執(zhí)行渲染
nextRenderExpirationTime !== NoWork && // 任務(wù)是個異步的涯塔,執(zhí)行到一半了肌稻,交還給瀏覽器執(zhí)行
expirationTime < nextRenderExpirationTime // 新的任務(wù)優(yōu)先級高于現(xiàn)在的任務(wù)
) {
// This is an interruption. (Used for performance tracking.)
interruptedBy = fiber; // 記錄
resetStack(); // 優(yōu)先執(zhí)行高優(yōu)先級任務(wù)
}
function resetStack() {
// nextUnitOfWork 被打斷的任務(wù)
if (nextUnitOfWork !== null) {
// 記錄,等待以后執(zhí)行
let interruptedWork = nextUnitOfWork.return;
while (interruptedWork !== null) {
// 退回任務(wù)
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
// 變回初始值匕荸,進(jìn)行新任務(wù)更新
nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestAbsoluteTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
}
何時執(zhí)行 requestWork
isWorking, isCommitting 是 react 渲染的兩個不同階段爹谭,
- isWorking
working 包含 committing(不可打斷) - isCommitting
fiberTree 的更新已經(jīng)結(jié)束,正在提交也就是更新dom 樹的渲染階段, 不可打斷
if (
!isWorking || // 沒有正在工作
isCommitting || // 或者正在提交榛搔,也就是更新dom 樹的渲染階段
nextRoot !== root // 不同的 root 一般不存在不同
) {
const rootExpirationTime = root.expirationTime; // 重新查找 root expirationTime诺凡,因為可能會改變
requestWork(root, rootExpirationTime); // 請求工作
}
requestWork
核心功能
- 將 root 節(jié)點加入到 root調(diào)度隊列中
- 判斷是否是批量更新
- 最后根據(jù) expirationTime 的類型判斷調(diào)度的類型
requestWork 流程
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime); // 把當(dāng)前 root設(shè)置為最高優(yōu)先級
// isRendering 調(diào)度已經(jīng)在執(zhí)行了, 循環(huán)已經(jīng)開始了
if (isRendering) {
return;
}
// 批量處理相關(guān)
// 調(diào)用 setState 時在 enqueueUpdates 前 batchedUpdates 會把 isBatchingUpdates 設(shè)置成 true
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, true);
}
return; // isBatchingUpdates true // 普通的 setState 在進(jìn)入 enqueueUpdates 時在這里直接不執(zhí)行了,下面其實沒進(jìn)入調(diào)度
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) { // 同步的調(diào)用 js 代碼
performSyncWork();
} else { // 異步調(diào)度 獨立的 react 模塊包践惑,利用瀏覽器有空閑的時候進(jìn)行執(zhí)行腹泌,設(shè)置 deadline 在此之前執(zhí)行
scheduleCallbackWithExpirationTime(root, expirationTime); // 在 secheduler 文件夾下的單獨模塊
}
}
addRootToSchedule
- 判斷當(dāng)前 root 是否調(diào)度過, 單個或多個 root 構(gòu)建成單向鏈表結(jié)構(gòu)
- 如果調(diào)度過,設(shè)置當(dāng)前任務(wù)優(yōu)先級最高
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
// root.nextScheduledRoot 用來判斷是否有異步任務(wù)正在調(diào)度, 為 null 時會增加 nextScheduledRoot
// 這個 root 還沒有進(jìn)入過調(diào)度
if (root.nextScheduledRoot === null) {
root.expirationTime = expirationTime;
// lastScheduledRoot firstScheduledRoot 是單向鏈表結(jié)構(gòu)尔觉,表示多個 root 更新
// 這里只有一個 root 只會在這里執(zhí)行
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
} else { // 有個多個root 時進(jìn)行單向鏈表的插入操作
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
}
} else {
// 傳入的 root 已經(jīng)進(jìn)入過調(diào)度, 把 root 的優(yōu)先級設(shè)置最高
const remainingExpirationTime = root.expirationTime;
// 如果 root 的 expirationTime 是同步或者優(yōu)先級低凉袱,增加為計算出的最高優(yōu)先級
if (
remainingExpirationTime === NoWork ||
expirationTime < remainingExpirationTime
) {
root.expirationTime = expirationTime; // 把當(dāng)前 root 的優(yōu)先級設(shè)置為當(dāng)前優(yōu)先級最高的
}
}
}
batchedUpdates 批量更新
- 每次 react 創(chuàng)建更新都會執(zhí)行 requestWork。如: setState
- 在 requestWork 中決定 react 的更新是異步調(diào)度還是同步執(zhí)行
setState 的調(diào)用
import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
export default class BatchedDemo extends React.Component {
state = {
number: 0,
}
handleClick = () => {
// 方法一
// 事件處理函數(shù)自帶`batchedUpdates`
// this.countNumber() // 執(zhí)行的結(jié)果是 0, 0, 0
// 方法二
// 主動`batchedUpdates`
setTimeout(() => {
this.countNumber() // 執(zhí)行的結(jié)果是 1侦铜,2专甩,3
}, 0)
// 方法三
// setTimeout中沒有`batchedUpdates`
// setTimeout(() => {
// batchedUpdates(() => this.countNumber()) // 執(zhí)行的結(jié)果是 0, 0, 0
// }, 0)
}
countNumber() {
const num = this.state.number
this.setState({
number: num + 1,
})
console.log(this.state.number)
this.setState({
number: num + 2,
})
console.log(this.state.number)
this.setState({
number: num + 3,
})
console.log(this.state.number)
}
render() {
return <button onClick={this.handleClick}>Num: {this.state.number}</button>
}
}
requestWork
- 當(dāng) setState 創(chuàng)建更新后進(jìn)入調(diào)度,執(zhí)行到 requestWork 里時會判斷一個 isBatchingUpdates 的全局變量钉稍。
- 在 requestWork 中斷點
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
debugger
// ...
if (isRendering) {
return;
}
// 批量處理相關(guān)
// 調(diào)用 setState 時在 enqueueUpdates 前 batchedUpdates 會把 isBatchingUpdates 設(shè)置成 true
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, true);
}
return; // isBatchingUpdates true // 普通的 setState 在進(jìn)入 enqueueUpdates 時在這里直接不執(zhí)行了配深,下面其實沒進(jìn)入調(diào)度
}
// 只有異步模式任務(wù)時才會執(zhí)行
}
- 在 requestWork 中斷點,發(fā)現(xiàn)在判斷 isBatchingUpdates 變量時就直接返回了嫁盲,雖然 expirationTime 是 Sync 但是下面的 performSyncWork() 并不會執(zhí)行。
-
setState 時先執(zhí)行了一個 batchedUpdates 的函數(shù)。
-
多次的 setState 在 enqueueUpdates 函數(shù)中羞秤,fiber 對象的 baseState 仍然是 0, 但是 fiber 對象上的 updateQueue 更新隊列上已經(jīng)記錄好了多次 update 對象將要更新 state 的 payload缸托。
batchedUpdates 的源碼
- setState 在 batchedUpdates 中先把 isBatchingUpdates 暫存為 previousIsBatchingUpdates, 再設(shè)置為 true 防止在 requestWork 中執(zhí)行。
- 在 try 代碼塊中執(zhí)行組件的方法 fn瘾蛋,fn 不論執(zhí)行多少次 setState 執(zhí)行完了都會通過 finally 進(jìn)入把 isBatchingUpdates 再設(shè)置回 false俐镐。
- 最后通過執(zhí)行 performSyncWork() 方法,而不是在 requestWork 中調(diào)用哺哼。
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates; // 初始為 false
isBatchingUpdates = true;
try {
return fn(a); // 執(zhí)行組件綁定的方法, 走到 requestWork 里
} finally {
// setState 最終 enqueueUpdates 全部走到 requestWork 后變回 false 再一同 performSyncWork 才真正的執(zhí)行并改變 state
isBatchingUpdates = previousIsBatchingUpdates; // 變回 false
// 如果是 setTimeout(() => { this.setState }) setTimeout 走到這里后才執(zhí)行 this.setState 這時上下文環(huán)境是 window isBatchingUpdates 已經(jīng) false佩抹,setState 就是同步的
if (!isBatchingUpdates && !isRendering) {
performSyncWork(); // 當(dāng)所有 setState 執(zhí)行完全部enqueueUpdates 后代替 requestWork 來調(diào)度
}
}
}
方法二 setTimout 執(zhí)行方式
setTimeout(() => {
batchedUpdates(() => this.countNumber())
}, 0)
- setTimeout 等瀏覽器 API 執(zhí)行的方式結(jié)果都會把三次 setState 結(jié)算的結(jié)果打印出來,像是一種同步的執(zhí)行方式
- 再次 debugger 取董,這時在 batchedUpdates 函數(shù)中的 fn 的執(zhí)行內(nèi)容只是 setTimeout棍苹。
-
當(dāng) setTimeout 執(zhí)行完后直接進(jìn)入了 finally 代碼塊中,isBatchingUpdates 變回了 false
- 當(dāng) setTimeout 結(jié)束執(zhí)行回調(diào)中的 setState 進(jìn)入 requestWork 時 isBatchingUpdates 已經(jīng)變?yōu)?false茵汰,requestWork 將會執(zhí)行下去枢里,最終執(zhí)行自己 performSyncWork()
-
三次 setState 都會通過 requestWork 執(zhí)行 performSyncWork(),而不是之前通過 batchedUpdates 執(zhí)行一次蹂午,所以每次 setState 的 update 都會立刻改變 state栏豺,結(jié)果也是同步的輸出。
方法三 使用 batchedUpdates API
batchedUpdates 讓 setState 的更新仍然為批量更新
setTimeout(() => {
batchedUpdates(() => this.countNumber())
}, 0)
- batchedUpdates API 其實就是 batchedUpdates 函數(shù)
- setTimeout 執(zhí)行回調(diào)時 batchedUpdates API 又把 isBatchingUpdates 設(shè)置為 true豆胸,讓 多次的 setState 又能進(jìn)行批量更新奥洼。
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates; // 初始為 false
isBatchingUpdates = true;
try {
return fn(a); // 執(zhí)行組件綁定的方法, 走到 requestWork 里
} finally {
isBatchingUpdates = previousIsBatchingUpdates; // 變回 false
if (!isBatchingUpdates && !isRendering) {
performSyncWork(); // 當(dāng)所有 setState 執(zhí)行完全部enqueueUpdates 后代替 requestWork 來調(diào)度
}
}
}
總結(jié) setState 是同步還是異步
- setState 本身的方法調(diào)用時同步的,但是調(diào)用 setState 不表示 state 立即更新的晚胡,state 的更新是根據(jù)我們執(zhí)行環(huán)境的上下文來判斷的灵奖。
- 如果處于批量更新的情況下 state 就不是立即更新的,如果不處于批量更新情況下有可能立即更新.
- 現(xiàn)在有 asyncMode 異步渲染的情況搬泥,state 也不是立即更新的桑寨,需要進(jìn)入異步調(diào)度的過程。