render過程
我們想了解react的工作機制,我們直接去看源碼很難去弄懂每一步到底是做什么的触幼,在復(fù)雜的函數(shù)調(diào)用中我們很容易讓自己迷失,所以我決定跟隨一些常用方法來分析工作機制究飞,第一篇就是ReactDOM.render
這個入口方法,在講解中我會直接忽略dev和調(diào)試的代碼置谦,因為這與工作機制無關(guān)。
先來看下主要的流程
先來看一下入口代碼
/**
* 渲染dom的入口方法
* @param {*} element
* @param {*} container
* @param {*} callback
*/
export function render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
/**
* render方法真正調(diào)用的主方法
* 主要步驟有初次渲染亿傅,創(chuàng)建fiberroot對象->將更新
*/
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
// 首次渲染時不存在這個元素媒峡,初次渲染進(jìn)入這個邏輯
if (!root) {
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 初次渲染不需要批處理要立即同步更新
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 不是首次渲染,比如之后調(diào)用setState更新都會將更新加入隊列葵擎,等待事務(wù)調(diào)度更新
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
這里我們實際調(diào)用的是legacyRenderSubtreeIntoContainer谅阿,將我們傳入的組件也就是element掛載到傳入的dom元素上。
首先我們會獲取dom上的一個root元素,如果沒有證明我們是初次渲染签餐,如果不是調(diào)用更新的方法寓涨。
官網(wǎng)上有一段例子,我覺得能很好理解這個過程當(dāng)我們第一次執(zhí)行tick會走初次渲染的邏輯氯檐,后邊的我們會走更新的邏輯戒良,這也是為什么我們不用setState也能達(dá)到更新頁面的效果
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
// 重復(fù)調(diào)用
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
在react中我們將這個root叫fiberRoot元素,這是整個渲染樹唯一的根節(jié)點冠摄,上邊相應(yīng)的也會掛載很多屬性糯崎。這里我們先不去看這個數(shù)據(jù)結(jié)構(gòu)。只看大體流程這里我們將的是render所以只說初次渲染的邏輯
我們實際會在unbatchedUpdates
中調(diào)用updateContainer
這個unbatchedUpdates
實際上是一種強制同步更新的方法我們先看源碼河泳。這里我們其實就是處理了傳入函數(shù)的executionContext
上下文
executionContext &= ~BatchedContext;executionContext |= LegacyUnbatchedContext;
這里的意思就是我們要將LegacyUnbatchedContext
這種類型合并進(jìn)當(dāng)前上下文拇颅,在方法執(zhí)行完后再恢復(fù)之前的執(zhí)行環(huán)境。
當(dāng)在這種上下文的環(huán)境下react的更新會走同步的邏輯乔询,因為這是第一次更新樟插,用戶要盡快的看到頁面的內(nèi)容,所以不需要走異步更新的邏輯
/**
* 同步更新任務(wù)
*/
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
接下來我們就要看看這個updateContainer
了竿刁,這里我們進(jìn)入了更新的主邏輯黄锤。方便理解還是先貼出主要代碼。
/**
* 更新的主邏輯食拜,
* 計算過期時間->創(chuàng)建更新的update對象->加入到調(diào)度隊列->并開啟任務(wù)調(diào)度
*/
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// 獲取root上的根fiber對象
const current = container.current;
// 獲取當(dāng)前的時間節(jié)點
const currentTime = requestCurrentTimeForUpdate();
// 計算當(dāng)前的到期時間
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
// 處理context相關(guān)
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 生成update對象鸵熟,是批處理更新的一個單元
const update = createUpdate(expirationTime, suspenseConfig);
// 為update對象具體要更新的參數(shù)賦值,傳入的是ReactElement元素
update.payload = {element};
// 將update將入fiber根對象上的任務(wù)隊列
enqueueUpdate(current, update);
// 開始執(zhí)行任務(wù)調(diào)度负甸,在到期時間內(nèi)
scheduleWork(current, expirationTime);
return expirationTime;
}
主要流程就是如下步驟 計算過期時間->創(chuàng)建更新的update對象->加入到調(diào)度隊列->并開啟任務(wù)調(diào)度
什么是過期時間流强,說到這里要先說下react16之后的新概念fiber,能支持我們在執(zhí)行耗時任務(wù)的時候可以跳出來相應(yīng)一些高優(yōu)先級的事件呻待,比如我們在一個循環(huán)中執(zhí)行一些復(fù)雜計算打月。但這時候用戶通過input打字,我們就要即時響應(yīng)輸入操作蚕捉,這在原來是做不到的奏篙。我們來實現(xiàn)這個功能主要靠的就是expirationTime過期時間這個概念。保證任務(wù)要在這個時間段內(nèi)完成迫淹,如果超時了那么就要立即在下一個事件循環(huán)中完成
然后就是生成一個update對象用來記錄更新的內(nèi)容秘通,將這個update對象插入rootFiber上的更新隊列(基于鏈表實現(xiàn))
最后開啟任務(wù)調(diào)度,這里render的執(zhí)行階段就執(zhí)行完了敛熬,接下來的任務(wù)就交給react的任務(wù)調(diào)度器去完成這也是下一篇要說的
expirationTime
先來看看關(guān)于過期時間的計算
// 值越大優(yōu)先級越高
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
// 計算高優(yōu)先級的時間
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
// 計算低優(yōu)先級的時間也就是過期時間
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
這里主要用的就是computeAsyncExpiration
和computeInteractiveExpiration
這個兩個不同優(yōu)先級時間的計算肺稀,關(guān)于ceiling
的計算我這里有一個例子
ceiling(10011, 10)//10020
ceiling(10019, 10)//10020
可以看到在計算值的時候會在每10個時間間隔內(nèi)的過期時間都相同,相對的Async的間隔為25应民,而Interactive的時間間隔為10话原。這也保證了在這個時間間隔內(nèi)的時間都會有相同的過期時間炸茧,這保證了在這段時間內(nèi)觸發(fā)的任務(wù)的優(yōu)先級相同。保證一同觸發(fā)的任務(wù)同時完成
再來看看當(dāng)前時間的獲取
/**
* 計算當(dāng)前時間
*/
export function requestCurrentTimeForUpdate() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
// 執(zhí)行的上下文是render或者commit稿静,在執(zhí)行階段獲取真實時間
return msToExpirationTime(now());
}
// We're not inside React, so we may be in the middle of a browser event.
// 如果我們沒在react內(nèi)部更新中梭冠,可能是在執(zhí)行瀏覽器的任務(wù)中
if (currentEventTime !== NoWork) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
// This is the first update since React yielded. Compute a new start time.
// 之前的任務(wù)已經(jīng)執(zhí)行完,開啟新的任務(wù)時候需要重新計算時間
currentEventTime = msToExpirationTime(now());
return currentEventTime;
}
首先在render和commit階段我們直接獲取當(dāng)前真實時間改备。
然后如果當(dāng)前有任務(wù)在執(zhí)行我們返回之前計算的當(dāng)前時間控漠,這也就確保了幾毫秒之內(nèi)觸發(fā)任務(wù)我們會以相同的當(dāng)前時間計算。
最后如果沒有任務(wù)我們計算一個新的當(dāng)前時間并賦給全局變量悬钳。
然后就是過期時間的計算了在初次渲染時會直接返回同步更新的標(biāo)識