轉前端一年半了,平時接觸最多的框架就是React
。在熟悉了其用法以後,避免不了想深刻了解其實現原理,網上相關源碼分析的文章挺多的,可是總感受不如本身閱讀理解來得深入。因而話了幾個週末去了解了一下經常使用的流程。也是經過這篇文章將本身的我的理解分享出來。html
在具體的源碼流程分析以前,根據我的理解,結合網上比較好的文章,先來分析一些概念性的東西。後續再分析具體的流程邏輯。前端
React 15
版本(Fiber之前)整個更新渲染流程分爲兩個部分:node
Reconciler
(協調器); 負責找出變化的組件Renderer
(渲染器); 負責將變化的組件渲染到頁面上在React
中能夠經過setState
、forceUpdate
、ReactDOM.render
來觸發更新。每當有更新發生時,Reconciler
會作以下工做:react
render
方法,將返回的JSX
轉化爲虛擬DOM
DOM
和上次更新時的虛擬DOM
對比DOM
Renderer
將變化的虛擬DOM渲染到頁面上在對某個更新節點執行玩Reconciler
以後,會通知Renderer
根據不一樣的"宿主環境"進行相應的節點渲染/更新。git
React 15
的diff
過程是 遞歸執行更新 的。因爲是遞歸,一旦開始就"沒法中斷" 。當層級太深或者diff
邏輯(鉤子函數裏的邏輯)太複雜,致使遞歸更新的時間過長,Js
線程一直卡主,那麼用戶交互和渲染就會產生卡頓。看個例子: count-demogithub
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>4<li>
<li>3<li> -> <li>6<li>
複製代碼
當點擊button
後,列表從左邊的一、二、3
變爲右邊的二、四、6
。每一個節點的更新過程對用戶來講基本是同步,但實際上他們是順序遍歷的。具體步驟以下:算法
button
,觸發更新Reconciler
檢測到<li1>
須要變動爲<li2>
,則馬上通知Renderer
更新DOM
。列表變成二、二、3
Reconciler
檢測到<li2>
須要變動爲<li4>
,通知Renderer
更新DOM
。列表變成二、四、3
Reconciler
檢測到<li3>
須要變動爲<li6>
,則馬上通知Renderer
更新DOM
。列表變成二、四、6
今後可見 Reconciler
和Renderer
是交替工做 的,當第一個節點在頁面上已經變化後,第二個節點再進入Reconciler
。因爲整個過程都是同步的,因此在用戶看來全部節點是同時更新的。若是中斷更新,則會在頁面上看見更新不徹底的新的節點樹!api
假如當進行到第2步的時候,忽然由於其餘任務而中斷當前任務,致使第三、4步沒法進行那麼用戶就會看到:數組
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>2<li>
<li>3<li> -> <li>3<li>
複製代碼
這種狀況是React
絕對不但願出現的。可是這種應用場景又是十分必須的。想象一下,用戶在某個時間點進行了輸入事件,此時應該更新input
內的內容,可是由於一個不在當前可視區域的列表的更新致使用戶的輸入更新被滯後,那麼給用戶的體驗就是卡頓的。所以React
團隊須要尋找一個辦法,來解決這個缺陷。瀏覽器
React15架構不能支撐異步更新以致於須要重構,因而React16架構改爲分爲三層結構:
React 15
對React 16
提出的需求是Diff更新應爲可中斷的,那麼此時又出現了兩個新的兩個問題:中斷方式和判斷標準;
React
團隊採用的是 合做式調度,即主動中斷和控制器出讓。判斷標準爲超時檢測。同時還須要一種機制來告知中斷的任務在什麼時候恢復/從新執行。 React
借鑑了瀏覽器的requestIdleCallback
接口,當瀏覽器有剩餘時間時通知執行。
因爲一些緣由React
放棄使用rIdc
,而是本身實現了功能更完備的polyfill
,即Scheduler
。除了在空閒時觸發回調的功能外,Scheduler
還提供了多種調度優先級供任務設置。
在React 15
中Reconciler
是遞歸處理Virtual DOM
的。而React16
使用了一種新的數據結構:Fiber
。Virtual DOM
樹由以前的從上往下的樹形結構,變化爲基於多向鏈表的"圖"。
更新流程從遞歸變成了能夠中斷的循環過程。每次循環都會調用shouldYield()
判斷當前是否有剩餘時間。源碼地址。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
複製代碼
前面有分析到React 15
中斷執行會致使頁面更新不徹底,緣由是由於Reconciler
和Renderer
是交替工做的,所以在React 16
中,Reconciler
與Renderer
再也不是交替工做。當Scheduler
將任務交給Reconciler
後,Reconciler
只是會爲變化的Virtual DOM
打上表明增/刪/更新的標記,而不會發生通知Renderer
去渲染。相似這樣:
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
複製代碼
只有當全部組件都完成Reconciler
的工做,纔會統一交給Renderer
進行渲染更新。
Renderer
根據Reconciler
爲Virtual DOM
打的標記,同步執行對應的渲染操做。
對於咱們在上一節使用過的例子,在React 16
架構中整個更新流程爲:
setState
產生一個更新,更新內容爲:state.count
從1
變爲2
Scheduler
,Scheduler
發現沒有其餘更高優先任務,就將該任務交給Reconciler
Reconciler
接到任務,開始遍歷Virtual DOM
,判斷哪些Virtual DOM
須要更新,爲須要更新的Virtual DOM
打上標記Reconciler
遍歷完全部Virtual DOM
,通知Renderer
Renderer
根據Virtual DOM
的標記執行對應節點操做其中步驟二、三、4隨時可能因爲以下緣由被中斷:
因爲Scheduler
和Reconciler
的工做都在內存中進行,不會更新頁面上的節點,因此用戶不會看見更新不徹底的頁面。
React的Diff
是有必定的 前提假設 的,主要分爲三點:
Virtual DOM
樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。ID
區分節點不管是
JSX
格式仍是React.createElement
建立的React組件最終都會轉化爲Virtual DOM
,最終會根據層級生成相應的Virtual DOM
樹形結構。React 15
每次更新會成新的Virtual DOM
,而後通 遞歸 的方式對比新舊Virtual DOM
的差別,獲得對比後的"更新補丁",最後映射到真實的DOM
上。React 16
的具體流程後續會分析到
React源碼很是多,並且16之後的源碼一直在調整,目前Github上最新源碼都是保留
xxx.new.js
與xxx.old.js
兩份代碼。react源碼 是採用Monorepo
結構來進行管理的,不一樣的功能分在不一樣的package
裏,惟一的壞處可能就是方法地址索引發來不是很方便,若是不是對源碼比較熟悉的話,某個功能點可能須要經過關鍵字全局查詢而後去一個個排查。開始以前,能夠先閱讀下官方的這份閱讀指南
由於源碼實在是太多太複雜了,全部我這裏儘量的最大到小,從面到點的一個個分析。大體的流程以下:
JSX
或者createElement
編碼的代碼到底會轉成啥ReactDOM.render
setState
更新的流程Scheduler
、Reconciler
、Renderer
的大體流程觸發渲染更新的操做除了
ReactDOM.render
、setState
外,還有forceUpdate
。可是實際上是差很少的,最大差別在於forceUpdate
不會走shouldComponentUpdate
鉤子函數。
先來看一個最簡單的JSX
格式編碼的組件,這裏藉助babel
進行代碼轉換,代碼看這
// JSX
class App extends React.Component {
render() {
return <div />
}
}
// babel
var App = /*#__PURE__*/function (_React$Component) {
_inherits(App, _React$Component);
var _super = _createSuper(App);
function App() {
_classCallCheck(this, App);
return _super.apply(this, arguments);
}
_createClass(App, [{
key: "render",
value: function render() {
return /*#__PURE__*/React.createElement("div", null);
}
}]);
return App;
}(React.Component);
複製代碼
關鍵點在於render
方法其實是調用了React.createElement
方法。那麼接下來咱們只須要分析createElement
作了啥便可。咱們先看看ReactElement
的結構:
let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol === 'function' && Symbol.for) {
REACT_ELEMENT_TYPE = Symbol.for('react.element');
}
const ReactElement = function (type, key, ref, props) {
const element = {
// 惟一地標識爲React Element,防止XSS,JSON裏不能存Symbol
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
}
return element;
}
複製代碼
很簡單的一個數據結構,每一個屬性的做用都一目瞭然,就不一一解釋了。而後分析React.createElement
源碼。
這裏多提一句,源碼註釋裏爲何說
$$typeof
可以有效防護JSX呢?通常來講,咱們編碼的DOM
都會轉爲ReactElement
的對象,可是React
提供了dangerouslySetInnerHTML
來做爲innerHTML
的替代方案,當攻擊者在服務端的資源中插入一段dangerouslySetInnerHTML
的JSON
的時候,會由於沒有$$typeof
而被React
判斷爲無效。由於Symbol
沒法JSON
化呀;具體檢測的源碼看這裏
const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
if (config !== null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
}
// 過濾React保留的關鍵字
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
// 遍歷children
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 設置默認props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(type, key, ref, props);
}
複製代碼
註釋應該已經夠清楚了哈。總結下來就是根據參數來生成一個ReactElement
對象,並綁定對應的props
、key
、ref
等;
開始正式流程分析以前,但願你對Fiber
有過必定的瞭解。若是沒有,建議你先看看這則視頻。而後,先來熟悉下ReactFiber
的大概結構。
export type Fiber = {
// 任務類型信息;
// 好比ClassComponent、FunctionComponent、ContextProvider
tag: WorkTag,
key: null | string,
// reactElement.type的值,用於reconciliation期間的保留標識。
elementType: any,
// fiber關聯的function/class
type: any,
// any類型!! 通常是指Fiber所對應的真實DOM節點或對應組件的實例
stateNode: any,
// 父節點/父組件
return: Fiber | null,
// 第一個子節點
child: Fiber | null,
// 下一個兄弟節點
sibling: Fiber | null,
// 變動狀態,好比刪除,移動
effectTag: SideEffectTag,
// 用於連接新樹和舊樹;舊->新,新->舊
alternate: Fiber | null,
// 開發模式
mode: TypeOfMode,
// ...
};
複製代碼
每一次經過ReactDom.render
渲染的一棵樹或者一個應用都會初始化一個對應的FiberRoot
對象做爲應用的起點。其數據結構以下ReactFiberRoot
。
type BaseFiberRootProperties = {
// The type of root (legacy, batched, concurrent, etc.)
tag: RootTag,
// root節點,ReactDOM.render()的第二個參數
containerInfo: any,
// 持久更新會用到。react-dom是整個應用更新,用不到這個
pendingChildren: any,
// 當前應用root節點對應的Fiber對象
current: Fiber,
// 當前更新對應的過時時間
finishedExpirationTime: ExpirationTime,
// 已經完成任務的FiberRoot對象,在commit(提交)階段只會處理該值對應的任務
finishedWork: Fiber | null,
// 樹中存在的最舊的未到期時間
firstPendingTime: ExpirationTime,
// 掛起任務中的下一個已知到期時間
nextKnownPendingLevel: ExpirationTime,
// 樹中存在的最新的未到期時間
lastPingedTime: ExpirationTime,
// 最新的過時時間
lastExpiredTime: ExpirationTime,
// ...
};
複製代碼
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 不肯定類型;多是class或function
export const HostRoot = 3; // 樹的根
export const HostPortal = 4; // 一顆子樹
export const HostComponent = 5; // 原生節點;根據環境而定,瀏覽器環境就是div等
export const HostText = 6; // 純文本節點
export const Fragment = 7;
複製代碼
到React 16.13.1
版本位置,內置的開發模式有以下幾種:
export type TypeOfMode = number;
// 普通模式,同步渲染,React15-16的生產環境用
export const NoMode = 0b0000;
// 嚴格模式,用來檢測是否存在廢棄API(會屢次調用渲染階段生命週期),React16-17開發環境使用
export const StrictMode = 0b0001;
// ConcurrentMode 模式的過渡版本
export const BlockingMode = 0b0010;
// 併發模式,異步渲染,React17的生產環境用
export const ConcurrentMode = 0b0100;
// 性能測試模式,用來檢測哪裏存在性能問題,React16-17開發環境使用
export const ProfileMode = 0b1000;
複製代碼
本文只分析 ConcurrentMode 模式
ReactDOM.render
使用參考這裏
通常來講,使用React
編寫應用,ReactDOM.render
是咱們觸發的第一個函數。那麼咱們先從ReactDOM.render
這個入口函數開始分析render
的整個流程。
源碼中會頻繁出現針對
hydrate
的邏輯判斷和處理。這個是跟SSR
結合客戶端渲染相關,不會作過多分析。源碼部分我都會進行省略
ReactDOM.render
實際上對ReactDOMLegacy
裏的render
方法的引用,精簡後的邏輯以下:
export function render( // React.creatElement的產物 element: React$Element<any>, container: Container, callback: ?Function, ) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
複製代碼
實際上調用的是legacyRenderSubtreeIntoContainer
方法,再來看看這個咯
function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, // 通常爲null children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// [Q]: 初始化容器。清空容器內的節點,並建立FiberRoot
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// FiberRoot; 應用的起點
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// [Q]: 初始化不能批量處理,即同步更新
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 省略... 更上面相似,差異是無需初始化容器和可批處理
// [Q]:咦? unbatchedUpdates 有啥奧祕呢
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
複製代碼
根據官網的使用文檔可知,在這一步會先清空容器裏現有的節點,若是有異步回調callback
會先保存起來,並綁定對應FiberRoot
引用關係,以用於後續傳遞正確的根節點。註釋裏我標註了兩個[Q]
表明兩個問題。咱們先來仔細分析這兩個問題
從命名上看,legacyCreateRootFromDOMContainer
是用來初始化根節點的。 將legacyCreateRootFromDOMContainer
的返回結果賦值給container._reactRootContainer
,而_reactRootContainer
從代碼上看是做爲是否已經初始化的依據,也驗證了這一點。不信的話,打開你的React
應用,查看下容器元素的_reactRootContainer
屬性
function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, ): RootType {
// 省略 hydrate ...
return createLegacyRoot(container, undefined);
}
export function createLegacyRoot( container: Container, options?: RootOptions, ): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
function ReactDOMBlockingRoot( container: Container, tag: RootTag, options: void | RootOptions, ) {
// !!! look here
this._internalRoot = createRootImpl(container, tag, options);
}
複製代碼
一連串的函數調用,其實就是還回了一個ReactDOMBlockingRoot實例。其中重點在於屬性_internalRoot
是經過createRootImpl
建立的產物。
function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) {
// 省略 hydrate ...
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// 省略 hydrate ...
return root;
}
export function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot {
// 生成 FiberRoot
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// 爲Root生成Fiber對象
const uninitializedFiber = createHostRootFiber(tag);
// 綁定 FiberRoot 與 Fiber
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 生成更新隊列
initializeUpdateQueue(uninitializedFiber);
return root;
}
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
baseQueue: null,
shared: {
pending: null,
},
effects: null,
};
fiber.updateQueue = queue;
}
複製代碼
大體邏輯就是生成了一個FiberRoot
對象root
。並生成了root
對應的Fiber
對象,同時生成了該fiber
的更新隊列。從這裏清楚的知道了FiberRoot
是在什麼時候初始化的,咱們得先記住這個FiberRoot
,能夠認爲他是整個React
應用的起點。
源碼中的英文註釋說明這裏是無需批處理,應該當即執行。其傳入參數是一個執行updateContainer
的包裝函數。 可是在else
判斷中實際上也執行了updateContainer
。那麼unbatchedUpdates
有啥奧祕呢?
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
// !!! look here
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}
export function flushSyncCallbackQueue() {
// 省略...
flushSyncCallbackQueueImpl();
}
// 清空同步任務隊列
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
isFlushingSyncQueue = true;
let i = 0;
try {
const isSync = true;
const queue = syncQueue;
// 以最高優先級來清空隊列裏的任務
runWithPriority(ImmediatePriority, () => {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// 移除錯誤的任務
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// 在下一個執行單元恢復執行
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
isFlushingSyncQueue = false;
}
}
}
複製代碼
在unbatchedUpdates
中,其實就是多了一段finally
中的邏輯。其中的邏輯主要是刷新同步任務隊列。想想,爲啥呢?那麼說明在fn(a)
的執行過程當中確定產生了同步任務唄!那麼接下來繼續跟進到updateContainer
中瞧一瞧。
注意,這裏updateContainer
已是屬於Reconciler
流程了哦。繼續跟進:
export function updateContainer( element: ReactNodeList, // 要渲染的組件 container: OpaqueRoot, // OpaqueRoot就是FiberRoot parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTimeOpaque {
// 根節點Fiber
const current = container.current;
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// TODO:計算這次任務的過時時間
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 建立一個更新任務
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
update.payload = { element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 將任務插入Fiber的更新隊列
enqueueUpdate(current, update);
// 調度任務 scheduleWork爲scheduleUpdateOnFiber
scheduleWork(current, expirationTime);
return expirationTime;
}
複製代碼
這一步看上去代碼賊多,其實就是先計算出當前更新的過時時間,而後經過createUpdate
建立了一個update
更新任務,接着經過enqueueUpdate
插入 循環任務隊列,最後使用scheduleUpdateOnFiber
來調度任務。
從這裏開始,源碼中有同步和異步兩種處理方式,同步任務是不會通過
Scheduer
進行調度的。爲了分析的完整性,咱們只分析異步過程。後續頻繁提到的expirationTime
,能夠暫且認爲其爲任務的"過時時間節點",是具體的"時間點",而不是"時間長度"。可是在不一樣的階段其意義是不同的。能夠肯定的是,組件的更新與否或者說更新的時間節點是由其來決定的。
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTimeOpaque, ) {
// 獲取FiberRoot,並更新子Fiber的過時時間(父組件更新觸發子組件更新)
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
return null;
}
if (expirationTime === Sync) {
// 同步任務調度
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
// 省略...
}
複製代碼
scheduleUpdateOnFiber
只是用於 更新以當前節點爲Root的整個"樹"的過時時間。 其中重點在ensureRootIsScheduled
這個方法
// 此函數用於調度任務。 一個root(fiber節點)只能有一個任務在執行
// 若是已經有任務在調度中,將檢查已有任務的到期時間與下一級別任務的到期時間相同。
// 每次更新和任務退出前都會調用此函數
// 注意:root是FiberRoot
function ensureRootIsScheduled(root: FiberRoot) {
// lastExpiredTime表明過時時間
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime !== NoWork) {
// 特殊狀況:過時的工做應同步刷新
root.callbackExpirationTime = Sync;
root.callbackPriority = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
return;
}
// TODO:從暫停或等待的任務中取出優先級最高的任務的過時時間
// 就是從任務隊列中取出下次將執行的調度任務的過時時間?
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
// root有正在處理的調度任務
const existingCallbackNode = root.callbackNode;
if (expirationTime === NoWork) {
if (existingCallbackNode !== null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
}
return;
}
// 計算當前任務的過時時間; 同一事件中發生的全部優先級相同的更新都收到相同的到期時間
const currentTime = requestCurrentTimeForUpdate();
// 根據下一次調度任務的過時時間與當前任務的過時時間計算出當前任務的優先級
// 即currentTime小於expirationTime,那麼其優先級更高
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
);
// 若是當前正在處理的任務優先級基於這次任務,取消正在處理的任務!
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// 任務必須具備徹底相同的到期時間。
existingCallbackExpirationTime === expirationTime &&
// 比較兩次任務的優先級
existingCallbackPriority >= priorityLevel
) {
return;
}
// 取消調度任務
cancelCallback(existingCallbackNode);
}
// 更新到期時間與優先級
root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
let callbackNode;
if (expirationTime === Sync) {
// 省略...
// 這裏會將任務推入同步任務隊列,前面分析到 flushSyncCallbackQueueImpl 清空的任務就是從這裏推入
} else {
// 將任務推入Scheduler調度隊列
callbackNode = scheduleCallback(
priorityLevel,
// 綁定
performConcurrentWorkOnRoot.bind(null, root),
// 計算超時時間
{ timeout: expirationTimeToMs(expirationTime) - now() },
);
}
// 更新Fiber的當前回調節點
root.callbackNode = callbackNode;
}
複製代碼
ensureRootIsScheduled
中的主要邏輯分三步:
Scheduler
中的調度隊列,並設置其優先級與任務過時時間這段代碼每一段都是能夠去延伸開分析的。可是我這裏主要是分析大體流程,因此主要分析scheduleCallback
相關的邏輯。其餘部分,之後有時間在進一步分析。
scheduleCallback
是將任務的執行函數交由Scheduler
來處理。那麼後續的流程須要等待Scheduler
來觸發具體的執行函數performConcurrentWorkOnRoot
。關於render
的流程就先暫時分析到這裏爲止。
render
會調用legacyRenderSubtreeIntoContainer
方法legacyRenderSubtreeIntoContainer
中,若是是第一次渲染,會先初始化FiberRoot
,其爲應用的起點。同時生成根節點的Fiber
實例。這裏 FiberRoot.current
= Fiber
; Fiber.stateNode
= FiberRoot
。updateContainer
會計算出這次更新的過時時間。並生成任務對象update
,將其插入Fiber
中的更新隊列,而後調用scheduleUpdateOnFiber
觸發任務調度scheduleUpdateOnFiber
會更新以該Fiber節點爲根節點的整棵Fiber樹的過時時間。而後調用ensureRootIsScheduled
進行調度ensureRootIsScheduled
中會綁定任務與具體執行函數。而後交由Scheduler
處理在繼續分析後續的Reconciler
和Renderer
細節以前,咋們趁熱打鐵來熟悉下setState
的流程。既然調用的時候是經過this.setState
來調動的,那麼就從Component
裏面去找咯。來look
一下ReactBaseClasses
const emptyObject = {};
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
// ReactNoopUpdateQueue 是一個沒啥意義的空對象
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
Component
的初始結構很簡單。咱們看到其setState
方法就是調用了this.updater.enqueueSetState
方法,可是update
默認是空的無用對象,咱們通常也沒有在構造方法裏傳入一個update
參數,那麼說明這個方法確定是後續注入的咯。與是我找啊找,找到了一個差很少的東西classComponentUpdater
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 生成這次setState的更新對象
const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 更新任務入隊
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
// 同上相似
},
enqueueForceUpdate(inst, callback) {
// 同上相似
},
};
複製代碼
嘿嘿,是否是發現了enqueueSetState
裏的邏輯有點似曾相識。其實就是咱們以前分析render
流程中遇到的updateContainer
的流程是同樣的啦。不記得的話回頭再看看咯。那麼接下來咱們只要分析下classComponentUpdater
是怎麼注入爲Component
的update
屬性便可了。
前面分析render
流程的時候,咱們還只分析到了生成任務分片並推入調度隊列,尚未對組件的初始化有過度析。從Component
的構造函數中猜想是否是在初始化Component
的時候React
幫咱們注入的呢? 順着這個思路進行下一步的分析。首先咱們先來看beginWork
方法中的一段代碼,beginWork
方法在後面會具體分析。這裏先知道他是用於建立子組件的Fiber
對象便可。
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
// 嘗試複用 current 節點
if (current !== null) {
// 省略...
}
// 不能複用則 update 或者 mount
switch (workInProgress.tag) {
// 省略...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 省略...
}
}
複製代碼
beginWork
中的代碼分爲兩部分。分別用於處理mount
和update
的邏輯。咱們分析的流程是第一次初始化,那麼走的是mount
流程。beginWork
會根據不一樣的tag
調用不一樣的方法,這裏咱們先來看看updateClassComponent
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// 省略 context 的處理...
// 組件的實例
const instance = workInProgress.stateNode;
let shouldUpdate;
// instance爲null 說明組件第一次渲染
if (instance === null) {
if (current !== null) {
// 重置current與wip的依賴(備份)
current.alternate = null;
workInProgress.alternate = null;
// 標記爲新增節點
workInProgress.effectTag |= Placement;
}
// 初始化組件實例
constructClassInstance(workInProgress, Component, nextProps);
// 掛載; 並調用相應的生命週期
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else {
// 省略更新邏輯...
}
// TODO:執行 render 新建子Fiber。
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
複製代碼
function constructClassInstance( workInProgress: Fiber, ctor: any, props: any, ): any {
let context = emptyContextObject;
// 省略 context 相關邏輯...
const instance = new ctor(props, context);
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);
adoptClassInstance(workInProgress, instance);
// 省略 context 相關邏輯...
return instance;
}
複製代碼
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// 綁定實例與Fiber,方便後續更新使用
setInstance(instance, workInProgress);
}
複製代碼
能夠看到當instance
爲null
的時候,會執行如下幾個流程
effectTag
爲Placement
,表明爲新增節點Fiber(workInProgress)
上,並綁定update
屬性mountClassInstance
來掛載節點,並調用相關的生命週期。至此,後續的更新流程就跟render
流程一致的了,就不作重複分析啦~
Scheduler
是React
團隊針對任務調度單獨實現的一個rIdc
的polyfill
。React
團隊其意圖不只僅侷限於React
這一個應用場景,更想服務與更多的業務,成爲更普遍應用的一個工具。
既然任務具備不一樣的過時時間和優先級,那麼就須要一個數據結構來管理優先級任務。React
中expirationTime
越小的任務應該更優先處理,那麼這個數據結構顯然就是一個最小優先隊列啦。而React
是基於小頂堆來實現的最小優先隊列。仍是直接看代碼吧。SchedulerMinHeap
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
// 插入到堆末尾
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
// 獲取堆頂任務,sortIndex/id 最小的任務
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
// 刪除堆頂任務
export function pop(heap: Heap): Node | null {
const first = heap[0];
if (first !== undefined) {
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
// 向上維持小頂堆
function siftUp(heap, node, i) {
let index = i;
while (true) {
// 位運算;對應根據節點求其父節點-> i / 2 - 1
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (parent !== undefined && compare(parent, node) > 0) {
// parent 更大,交換位置
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
return;
}
}
}
// 向下維持小頂堆
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// // 若是左子節點或右子節點小於目標節點(父節點),則交換
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
// 先比較sort index,再比較 task id
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
複製代碼
具體實現就是用數組模擬了一個最小堆的結構。能夠看到,每次任務的插入或者移除都會從新回覆最小堆結構,排序規則以sortIndex
,taskId
爲輔。在React中sortIndex
對應的其實就是過時時間,taskId
則爲遞增任務序列。這一點後續會分析到。
前面有分析到在ensureRootIsScheduled
中會生成一個任務節點,而後經過scheduleCallback
將任務推入Scheduler
中。那麼咱們先從這個任務進隊的方法來逐步分析
var taskIdCounter = 1;
// 目前Scheduler對外的api都是unstate_級別的,表示不是穩定版本
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 實際是調用performance.now() 或者 Date.now() 前者更精確
var currentTime = getCurrentTime();
var startTime;
var timeout;
// 根據是否有延遲來肯定開始時間
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
// [Q1]:有超時配置直接用。不然根據優先級計算
timeout =
typeof options.timeout === 'number'
? options.timeout
: timeoutForPriorityLevel(priorityLevel);
} else {
timeout = timeoutForPriorityLevel(priorityLevel);
startTime = currentTime;
}
// 過時時間等於開始時間+超時時間
var expirationTime = startTime + timeout;
// 一個task的數據結構就是這樣啦。
var newTask = {
// 相同超時時間的任務會對比id,那就是先到先得咯
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// [Q2]:下面出現了一個延遲隊列(timerQueue)和一個任務隊列(taskQueue)
if (startTime > currentTime) {
// This is a delayed task.
// 說明這是一個延遲任務;即options.delay存在嘛
newTask.sortIndex = startTime;
// 若是開始時間大於當前時間,就將它 push 進這個定時器隊列,說明這個是一個等待隊列
push(timerQueue, newTask);
// 若是任務隊列爲空,說明全部任務都被延遲,且newTask是最先的延遲任務。
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 若是正在進行超時處理,先取消,後續再從新開始
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 發起一個超時處理
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
// 非延遲任務丟入任務隊列
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 若是沒在調度中則開啓調度;
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// [Q]開啓調度
requestHostCallback(flushWork);
}
}
// [A]:還回這個task的引用
return newTask;
}
複製代碼
從這段代碼能夠看到一個調度任務的數據結構是怎樣的,以及任務的排序依據sortIndex
其實就是任務的過時時間expirationTime
,而id
則是一個遞增序列。註釋中標註了幾個問題,下面一一具體分析
// 當即執行
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 用戶行爲阻塞
var USER_BLOCKING_PRIORITY = 250;
// 默認五秒過時時間
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不過時, maxSigned31BitInt爲v8 32爲系統最大有效數值
var IDLE_PRIORITY = maxSigned31BitInt;
function timeoutForPriorityLevel(priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
return IMMEDIATE_PRIORITY_TIMEOUT;
case UserBlockingPriority:
return USER_BLOCKING_PRIORITY;
case IdlePriority:
return IDLE_PRIORITY;
case LowPriority:
return LOW_PRIORITY_TIMEOUT;
case NormalPriority:
default:
return NORMAL_PRIORITY_TIMEOUT;
}
}
複製代碼
能夠看到,這裏將優先級轉換成了常量級的具體時間,優先級越高的timeout
時間越低。
在startTime > currentTime
的條件分支中,分別將任務推入了taskQueue
和timerQueue
。而這兩個隊列其實就是咱們前面分析到的一個最小堆的結構。taskQueue
表明當前正在調度的任務,而timerQueue
表明延遲任務隊列。在任務調度的過程當中,會不停的將timerQueue
中的任務轉移到taskQueue
中,這一步後續會分析到。
咱們看到當任務插入調度隊列時,若是此時不在調度中,會調用requestHostCallback
方法開啓調度,並傳入了一個flushwork
做爲入參函數。
requestHostCallback = function(callback) {
// 這裏將傳入的callback緩存起來了
scheduledHostCallback = callback;
// 是否在消息循環中
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
複製代碼
從代碼看彷佛rHC
的做用只是緩存了callback
即flushwork
這個入參函數。併發送了一個空的message
。那麼重點就在與這個port
是爲什麼物了。其實這裏就是React
如何模擬requestIdleCallback
的地方了。
不熟悉MessageChannel
的能夠先了解一下。先來看看Scheduler
中是如何用的。
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
複製代碼
能夠得知,當使用port.postMessage
發生消息的時候,實際處理消息的函數爲performWorkUntilDeadline
。
let isMessageLoopRunning = false;
let scheduledHostCallback = null;
const performWorkUntilDeadline = () => {
// scheduledHostCallback 具體是由 scheduledHostCallback 賦值的
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// [Q]:截止時間 = 當前時間 + yieldInterval
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// 是否還有剩餘任務。scheduledHostCallback 多是 flushwork
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
// 沒有更多任務 中止循環,並清楚scheduledHostCallback引用
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 若是還有任務,則繼續發消息。相似一個遞歸的操做
port.postMessage(null);
}
} catch (error) {
// 若是一個任務出錯了。直接跳過執行下一個任務,並拋出錯誤
port.postMessage(null);
throw error;
}
} else {
// 重置循環狀態
isMessageLoopRunning = false;
}
// [Q]: 目前不知道這是啥
needsPaint = false;
};
複製代碼
老樣子,這裏有幾個問題須要仔細分析下。
從名字和使用方法上來看,我覺着應該是表明任務的執行時間。
// 默認是5
let yieldInterval = 5;
forceFrameRate = function (fps) {
// ??? 看不起我144hz
if (fps < 0 || fps > 125) {
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing framerates higher than 125 fps is not unsupported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5;
}
};
複製代碼
forceFrameRate
是一個對外提供的api
接口,用於動態配置調度任務的執行週期。
let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduling !== undefined &&
navigator.scheduling.isInputPending !== undefined
) {
const scheduling = navigator.scheduling;
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// 沒有時間了。可能但願讓主線程讓出控制權,以便瀏覽器能夠執行高優先級任務,主要是繪製和用戶輸入
// 所以若是有繪製或者用戶輸入行爲,則應該讓出,放回true
// 若是二者都不存在,那麼能夠在保持響應能力的同時下降產量
// 可是存在非`requestPaint`發起的繪製狀態更新或其餘主線程任務(如網絡事件)
// 所以最終在某個臨界點仍是得讓出控制權
if (needsPaint || scheduling.isInputPending()) {
// 有待處理的繪製或用戶輸入
return true;
}
// 沒有待處理的繪製或輸入。但在達到最大產量間隔時也須要釋放控制權
return currentTime >= maxYieldInterval;
} else {
return false;
}
};
requestPaint = function () {
needsPaint = true;
};
} else {
shouldYieldToHost = function () {
return getCurrentTime() >= deadline;
};
requestPaint = function () { };
}
複製代碼
首先須要明確的是shouldYieldToHost
與requestPaint
是Scheduler
對外提供的接口函數。具體的使用後續會分析到位。
從代碼可知,deadline
的用途是用於在shouldYieldToHost
中 檢測調度是否超時。默認清空下是直接對比當前時間currentTime
與deadline
的值。可是,在支持navigator.scheduling
的環境下,React
會有更多的考慮,也就是瀏覽器繪製與用戶輸入要有限響應,不然能夠適當的延長調度時間。
到這裏先總結下調度啓動的過程,省得腦子糊了。
requestHostCallback
準備好要執行的任務scheduledHostCallback
requestHostCallback
開啓任務調度循環MessageChannel
接收消息,並調用performWorkUntilDeadline
執行任務performWorkUntilDeadline
中先計算這次調度的deadline
。而後執行任務performWorkUntilDeadline
。不然結束消息循環前面還只是分析了任務調度循環執行的邏輯。具體執行的任務是scheduledHostCallback
的引用函數flushWork
。
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod codepath.
// 官方註釋說,生成環境不會去catch workLoop拋出的錯誤
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
複製代碼
flushWork
的工做比較簡單。只是重置了一些標誌符,最終返回了workLoop
的執行結果。那麼重點確定在這個函數了。
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// [Q]: 這是做甚?
advanceTimers(currentTime);
// 取出頂端任務。即最優先的任務
currentTask = peek(taskQueue);
while (
currentTask !== null &&
// debug 用的,無論
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
// 任務未過時,而且當前調度的deadline到了,將任務放到下次調度週期進行; shouldYieldToHost
currentTask.expirationTime > currentTime &&
// 這兩個前面分析過了; hasTimeRemaining一直爲true,那還判斷有啥意義???
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// 計算當前任務是否已經超時
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
// [Q]: 執行callback,好比前面render流程分析到的 performConcurrentWorkOnRoot
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// continuationCallback 成立,則取代當前任務的callback
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
// continuationCallback 不成立,從任務隊列彈出
// 防止任務被其餘地方取出,得判斷一下
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
// em.... 又是它
advanceTimers(currentTime);
} else {
// 任務被取消了,彈出任務
// 回顧下ensureRootIsScheduled 中調用 cancelCallback 的狀況
pop(taskQueue);
}
// 再次從頂端取任務
// 注意:若是 continuationCallback 成立的話,是沒有pop當前任務的。這次取到的仍是當前任務
currentTask = peek(taskQueue);
}
// performWorkUntilDeadline 中判斷 hasMoreWork 的邏輯就是這裏啦!
if (currentTask !== null) {
return true;
} else {
// [Q]:檢測延遲隊列中的任務是否是過時
let firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
複製代碼
大體流程註釋已經很詳細了。老規矩,分析標註的幾個問題。
function advanceTimers(currentTime) {
// 遍歷 timerQueue 中的任務;將超時的任務轉移到 taskQueue 中去
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// 任務被取消
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// 超時任務轉移
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// 未過期的繼續掛起
return;
}
timer = peek(timerQueue);
}
}
複製代碼
wookLoop
函數入口第一次調用advanceTimers
是將任務從新梳理一下,刷新任務隊列。而以後每次在while
調用是 由於任務的執行是須要消耗必定的時間的,全部在執行完後須要從新刷新任務隊列。
首先continuationCallback
的產生是有callback
決定的。callback
的返回值多是一個函數,這表明着當前任務應該被從新處理一次。這裏先留個問題,後續在分析callback
的具體實現的時候,咱們再進一步分析
在wookLoop
的結尾,當currentTask === null
的時候,會去檢測延遲隊列中的任務是否已通過期。
requestHostTimeout = function (callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
// 從新梳理任務隊列
advanceTimers(currentTime);
// isHostCallbackScheduled 爲true。說明有新任務進來了
if (!isHostCallbackScheduled) {
// 若是上面的 advanceTimers 梳理了過時的延遲任務到任務隊列中,則執行
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
// 不然遞歸調用該方法
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
複製代碼
能夠看出,其實就是在任務隊列中的任務執行完成後。經過遞歸的方法從延遲隊列中查詢是否有過時任務,有的話則轉移到任務隊列中,並執行。
到這裏,Scheduler
從任務入列,到循環調度,到任務執行的完整過程就已經分析完成了。作個簡單的流程總結:
unstable_scheduleCallback
建立任務,若是任務是延遲的則推入延遲隊列timerQueue
,不然推入任務隊列taskQueue
requestHostTimeout
方法使用setTimeout
來 遞歸檢測任務是否過時。不然直接發起任務調度requestHostCallback
requestHostCallback
經過MessageChannel
的port2
發送消息給port1
,具體的處理函數爲performWorkUntilDeadline
performWorkUntilDeadline
會計算這次調度的deadline,同時使用 消息循環 來遞歸執行任務wookLoop
執行。其將任務從任務隊列taskQueue
堆頂依次取出執行。若是任務隊列清空,則調用requestHostTimeout
開啓遞歸檢測。分析完Scheduler
的邏輯後,接下來接着分析Reconciler
的邏輯。咱們老生常談的Diff
更新的邏輯大部分就是發生在Reconciler
階段,其中包含了大量的組件更新計算與優化。
上面分析了Scheduler
的調度過程。而具體在Scheduler
中的執行的callback
是performConcurrentWorkOnRoot
。咱們來看一看
// 被Scheduler調用的入口函數
function performConcurrentWorkOnRoot(root, didTimeout) {
// 重置
currentEventTime = NoWork;
if (didTimeout) {
// 任務已經超時
const currentTime = requestCurrentTimeForUpdate();
// 將過時時間標記爲當前,以在單個批處理中同步處理已過時的工做。
markRootExpiredAtTime(root, currentTime);
// 調度一個同步任務
ensureRootIsScheduled(root);
return null;
}
// 獲取下一個到期(更新)時間. 將以此做爲本次渲染的執行必要性判斷
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
if (expirationTime !== NoWork) {
const originalCallbackNode = root.callbackNode;
// TODO:刷新被動的Hooks
flushPassiveEffects();
// 若是根或到期時間已更改,則丟棄現有堆棧並準備新的堆棧。 不然,咱們將從中斷的地方繼續。
if (
root !== workInProgressRoot ||
expirationTime !== renderExpirationTime
) {
// [Q]: 重置數據;
// 設置 renderExpirationTime 爲expirationTime
// 複製 root.current 爲 workInProgress等
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
}
if (workInProgress !== null) {
// 省略...
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 省略...
}
if (workInProgress !== null) {
// 仍然有任務要作。說明是超時了,退出而不提交。
stopInterruptedWorkLoopTimer();
} else {
stopFinishedWorkLoopTimer();
const finishedWork: Fiber = ((root.finishedWork =
root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
// commit;開始 Renderer 流程
finishConcurrentRender(
root,
finishedWork,
workInProgressRootExitStatus,
expirationTime,
);
}
}
return null;
}
複製代碼
首先會判斷任務是否超時,若是超時則以同步的方式執行該任務,防止任務被中斷。若是沒有超時,則先在prepareFreshStack
中作一些初始化的工做。而後進入了workLoopConcurrent
循環。
// 本次渲染的到期時間
let renderExpirationTime: ExpirationTime = NoWork;
function prepareFreshStack(root, expirationTime) {
// 省略...
if (workInProgress !== null) {
// workInProgress 不爲空說明以前有中斷的任務。放棄
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
// 從current 複製 wip; 並重置effectList
workInProgress = createWorkInProgress(root.current, null);
// 設置renderExpirationTime爲下一個到期時間
renderExpirationTime = expirationTime;
// 省略...
}
複製代碼
若是當前wip
不爲空,說明上次有中斷的任務,經過不停向上回溯直到root
節點來取消中斷的任務。而後從 同時將前面從FiberRoot
中獲取下一個任務的到期時間,賦值給renderExpirationTime
做爲本次渲染的到期時間。
workLoopConcurrent
的代碼在本文開頭就貼出來過,這裏從新看下
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
// 第一次入參workInProgress爲FiberRoot的Fiber
// 後續將上一次返回值(子Fiber)做爲入參
workInProgress = performUnitOfWork(workInProgress);
}
}
複製代碼
workLoopConcurrent
的工做主要是循環對比current
和workInProgress
兩顆Fiber
樹。在wip
中爲變化的Fiber
打上effectTag
。同時會從下往上更新/建立DOM
節點,構成一顆離屏DOM
樹,最後交由Renderer
處理。
在熟悉流程以前,先貼出一個刪減版的代碼流程。這裏不按套路出牌,先根據我的理解作個總結。這樣帶着大體的思路結構可能會更好的去理解後續的源碼。
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 舊的 Fiber, 用於對比
const current = unitOfWork.alternate;
// 省略...
// [Q]: 處理當前Fiber節點,還回下一個子節點Fiber
let next = beginWork(current, unitOfWork, renderExpirationTime);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 沒有子節點
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
// 嘗試完成當前的Fiber,而後移至下一個同級。若是沒有更多的同級,返回父fiber。
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
workInProgress = unitOfWork;
do {
// 舊的 Fiber, 用於對比
const current = workInProgress.alternate;
const returnFiber = workInProgress.return;
// Check if the work completed or if something threw.
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// [Q]: 建立/更新當前Fiber對應的節點實例
let next = completeWork(current, workInProgress, renderExpirationTime);
stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress);
if (next !== null) {
// 產生了新的子節點
return next;
}
// [Q]:後面是在構建 effectList 的單向鏈表
// 先省略...
} else {
// 有異常拋出。根據是不是boundary來決策是捕獲仍是拋出異常
// 省略...
}
const siblingFiber = workInProgress.sibling;
// 是否存在兄弟節點
if (siblingFiber !== null) {
return siblingFiber;
}
workInProgress = returnFiber;
} while (workInProgress !== null);
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
複製代碼
首先執行beginWork
進行節點操做,以及建立子節點,子節點會返回成爲next
,若是有next
就返回。返回到workLoopConcurrent
以後,workLoopConcurrent
會判斷是否過時之類的,若是沒過時則再次調用該方法。
若是next
不存在,說明當前節點向下遍歷子節點已經到底了,說明這個子樹側枝已經遍歷完,能夠完成這部分工做了。執行completeUnitOfWork
,主要分一下幾個步驟
completeUnitOfWork
首先調用completeWork
建立/更新當前Fiber
對應的節點實例(如原生DOM節點)instance
,同時將已經更新的子Fiber
的實例插入到instance
構成一顆離屏渲染樹。Fiber
節點存在effectTag
則將其追加到effectList
中sibling
兄弟節點,有則返回該兄弟節點,由於這個節點可能也會存在子節點,須要經過beginWork
進行操做。root
節點或者在某一個節點發現有sibling
兄弟節點。root
,那麼其返回也是null
,表明整棵樹的遍歷已經結束了,能夠commit
了。若是中間遇到兄弟節點則同於第3
步文字表達可能不是很清楚,直接看一個例子:
workLoopConcurrent.png
執行順序爲:
文本節點「你好」 不會執行
beginWork/completeWork
,由於React
針對只有單一文本子節點的Fiber
,會特殊處理
1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork
複製代碼
beginWork
在前面分析setState
的時候已經分析過其中mount
階段對應的邏輯了。那麼這裏就只分析update
的邏輯了。先來看下beginWork
的大體工做。
/** * @param {*} current 舊的Fiber * @param {*} workInProgress 新的Fiber * @param {*} renderExpirationTime 下一次到期時間,即本次渲染有效時間 * @returns 子組件 Fiber */
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// 嘗試複用 current 節點
if (current !== null) {
// 省略...
// 複用 current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
workInProgress.expirationTime = NoWork;
// 不能複用則 update 或者 mount
switch (workInProgress.tag) {
// 省略...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
// 省略...
}
}
複製代碼
咱們接着以前分析過的updateClassComponent
來分析update
的流程。
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// 提早處理context邏輯。省略....
// 組件的實例
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// mount. wip.effectTag = Placement
// 省略...
} else {
// update. wip.effectTag = Update | Snapshot
// 調用 render 以前的生命週期,getDerivedStateFromProps | UNSAFE_componentWillReceiveProps(可能兩次)
// 接着調用shouldComponentUpdate判斷是否須要更新
// 最後更新props 和 state
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
// 執行 render 新建子Fiber。
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
function finishClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, ) {
// 引用應該更新,即便shouldComponentUpdate返回false
markRef(current, workInProgress);
const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
// 無需更新且沒有發送錯誤則直接複用current
if (!shouldUpdate && !didCaptureError) {
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
// 複用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren = instance.render();
// PerformedWork 提供給 React DevTools 讀取
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
// 出錯了。
// 省略...
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
workInProgress.memoizedState = instance.state;
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) {
if (current === null) {
// mount的組件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// update的組件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
複製代碼
最後還回的就是workInProgress.child
,跟beginWork
同樣,根據current === null
來區分mount
和update
。
實際上mountChildFibers
和reconcileChildFibers
均指向同一個函數reconcileChildFibers
。差異在於第二個參數currentFirstChild
。若是爲null
,則會去建立一個新的Fiber
對象,不然複用並更新props
。好比reconcileSingleElement
用於處理只有單個節點的狀況。
function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
//省略...
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// fiber節點對應的DOM節點是否存在
// update
if (current !== null && workInProgress.stateNode != null) {
// 爲 wip 計算出新的 updateQueue
// updateQueue 是一個奇數索引的值爲變化的prop key,偶數索引的值爲變化的prop value 的數組
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// mount
if (!newProps) {
return null;
}
const currentHostContext = getHostContext();
// 是否是服務端渲染
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// 省略...
} else {
// 生成真實DOM
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 將子孫DOM節點插入剛生成的DOM節點中,從下往上,構成一顆離屏DOM樹
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// 與updateHostComponent相似的處理 props
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
markRef(workInProgress);
}
}
return null;
}
//省略...
}
}
複製代碼
首先和beginWork
同樣,根據current === null
判斷是mount
仍是update
。
update
時,主要作了以下幾件事情,具體源碼diffProperties
:
STYLE prop
DANGEROUSLY_SET_INNER_HTML prop
CHILDREN prop
每次計算出新的prop
,都將其propKey
與nextProp
成對的保存在數組updatePayload
中。最後將updatePayload
賦值給wip.updateQueue
。
mount
時,處理的事情比較多,大體以下:
createInstance
: 爲Fiber
節點生成對應的真實DOM
節點appendAllChildren
: 將子孫DOM
節點插入剛生成的DOM
節點中。以此從下往上構成完整的DOM
樹finalizeInitialChildren
: 在setInitialProperties
中處理事件註冊。在setInitialDOMProperties
根據props
初始化DOM
屬性值的注意的是appendAllChildren
方法。因爲completeWork
屬於向上回溯的過程,每次調用appendAllChildren
時都會將已生成的子孫DOM
節點插入當前生成的DOM
節點下。那麼當回溯到根root
節點時,整個DOM
樹就都已經更新好了。
在每次completeWork
後,表明某個節點已經處理完成。前面說過,Reconciler
會爲發生改變的節點打上effectTag
,用於在Renderer
根據節點的effectTag
的執行具體更新。
所以在completeWork
的上層函數completeUnitOfWork
中(也就是以前省略的代碼),每執行完completeWork
會去維護一個effectList
的單向鏈表。若是當前Fiber
存在effectTag
,則插入鏈表。
// 構建 effectList 的單向鏈表
if (
returnFiber !== null &&
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// firstEffect 爲鏈表頭結點
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
// lastEffect 爲鏈表尾節點
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
const effectTag = workInProgress.effectTag;
// 跳過NoWork和PerformedWork tag。後者是提供給React Tools讀取
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
複製代碼
至此,Reconciler
流程結束。回頭再看看開頭的總結,是否是清楚一些了呢~
Commit
階段的代碼相對另外兩個來講是較爲簡單的。其入口在前面分析過的任務調度入口函數performConcurrentWorkOnRoot
中的結尾finishConcurrentRender
。最終調用的函數爲commitRootImpl
。看看代碼:
let nextEffect: Fiber | null = null;
function commitRootImpl(root, renderPriorityLevel) {
// 省略...
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
// commit不可中斷。 老是同步完成。
// 所以,如今能夠清除這些內容以容許安排新的回調。
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.nextKnownPendingLevel = NoWork;
// 省略...
// 獲取effectList
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}
if (firstEffect !== null) {
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 執行 snapshot = getSnapshotBeforeUpdate()
// 結果賦值爲 Fiber.stateNode.instance.__reactInternalSnapshotBeforeUpdate = snapshot
commitBeforeMutationEffects();
} while (nextEffect !== null);
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 根據Fiber.effectTag 執行具體的增刪改DOM操做
// 若是是卸載組件,還會調用 componentWillUnmount()
commitMutationEffects(root, renderPriorityLevel);
} while (nextEffect !== null);
// 省略...
nextEffect = firstEffect;
do {
// [Q]: 調用 render 後的生命週期
// current === null ? componentDidMount : componentDidUpdate
commitLayoutEffects(root, expirationTime);
} while (nextEffect !== null);
stopCommitLifeCyclesTimer();
nextEffect = null;
// 告訴Scheduler在幀末尾中止調度,這樣瀏覽器就有機會繪製。
requestPaint();
// 省略...
} else {
// 省略...
}
// 省略...
return null;
}
複製代碼
省略了許多的代碼,留下主要的內容。主要邏輯就是拿到Reconciler
維護的effectList
鏈表後,三次遍歷該鏈表,分別作的是:
Snapsshot
;用於componentDidUpdate
的第三個參數Fiber.effectTag
對組件或DOM執行具體操做完整代碼看commitBeforeMutationLifeCycles
,其中tai爲ClassComponent
的組件主要邏輯以下:
const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
複製代碼
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
// 把節點的文字內容設置爲空字符串
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 把ref置空,後續會設置ref,因此以前ref上的值須要先清空
commitDetachRef(current);
}
}
let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// 從effectTag中清除Placement標記
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// componentWillUnmount
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
// 省略...
}
nextEffect = nextEffect.nextEffect;
}
}
複製代碼
好像也沒啥好說的。值得注意的是,開始前會先調用commitDetachRef
將ref
的引用清除。而後針對不一樣的effectTag
執行不一樣的DOM
操做。
commitPlacement
; 新增節點。其中節點插入位置的計算算法能夠看下;commitWork
; 根據Reconciler
在diffProperties
計算出來的updateQueue
數組進行DOM
更新commitDeletion
; 這一步會從上往下依次調用該子樹下每一個組件的componentWillUnmount
函數function commitLayoutEffects( root: FiberRoot, committedExpirationTime: ExpirationTime, ) {
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void {
switch (finishedWork.tag) {
// ...
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 調用setState註冊的回調函數
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
// ...
}
}
複製代碼
仍是遍歷每一個Fiber
節點。若是是ClassComponent
,須要調用生命週期方法。同時對於更新的ClassComponent
,須要判斷調用的setState
是否有回調函數,若是有的話須要在這裏一塊兒調用。最後會調用commitAttachRef
更新ref
引用。
Commit
階段的流程到這裏也就結束了。
說實話,React
的源碼是在是真的多。想完徹底全細節分析到每個點,須要大量的時間和精力。本文也只是分析了一個大體的流程,不少細節之處沒有分析到位。後續會再花點時間針對一些細節問題作下探索。說到底,目前也只從面到面,而沒有達到從面到點分析的效果。許多觀點是我的的理解,寫出來是以供學習交流,有不妥之處,還請提提意見。