React深入分析更新的創(chuàng)建源碼
React
的鮮活生命起源于 ReactDOM.render
,這個過程會為它的一生儲備好很多必需品,我們順著這個線索,一探嬰兒般 React
應用誕生之初的悅?cè)弧?/p>
更新創(chuàng)建的操作我們總結(jié)為以下兩種場景
- ReactDOM.render
- setState
- forceUpdate
ReactDom.render
串聯(lián)該內(nèi)容,一圖以蔽之
首先看到 react-dom/client/ReactDOM
中對于 ReactDOM
的定義,其中包含我們熟知的方法、不穩(wěn)定方法以及即將廢棄方法。
const ReactDOM: Object = {createPortal,// LegacyfindDOMNode,hydrate,render,unstable_renderSubtreeIntoContainer,unmountComponentAtNode,// Temporary alias since we already shipped React 16 RC with it.// TODO: remove in React 17.unstable_createPortal(...args) {// ...return createPortal(...args);},unstable_batchedUpdates: batchedUpdates,flushSync: flushSync,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {// ...}, };
此處方法均來自 ./ReactDOMLegacy
,render
方法定義很簡單,正如我們常使用的那樣,第一個參數(shù)是組件,第二個參數(shù)為組件所要掛載的DOM節(jié)點,第三個參數(shù)為回調(diào)函數(shù)。
export function render( element: React$Element<any>,container: DOMContainer,callback: ?Function, ) {// ...return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,); } function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>,children: ReactNodeList,container: DOMContainer,forceHydrate: boolean,callback: ?Function, ) {// TODO: Without `any` type, Flow says "Property cannot be accessed on any// member of intersection type." Whyyyyyy.let root: RootType = (container._reactRootContainer: any);let fiberRoot;if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate,);fiberRoot = root._internalRoot;if (typeof callback === 'function') {const originalCallback = callback;callback = function() {const instance = getPublicRootInstance(fiberRoot);originalCallback.call(instance);};}// 初次渲染,不會將更新標記為batched.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);};}// UpdateupdateContainer(children, fiberRoot, parentComponent, callback);}return getPublicRootInstance(fiberRoot); }
這段代碼我們不難發(fā)現(xiàn),調(diào)用 ReactDOM.render
時,返回的 parentComponent
是 null,并且初次渲染,不會進行批量策略的更新,而是需要盡快的完成。(batchedUpdates批量更新后續(xù)介紹)
從這部分源碼我們不難看出,render
和 createProtal
的用法的聯(lián)系,通過DOM容器創(chuàng)建Root節(jié)點的形式
function legacyCreateRootFromDOMContainer( container: DOMContainer,forceHydrate: boolean, ): RootType {const shouldHydrate =forceHydrate || shouldHydrateDueToLegacyHeuristic(container);// First clear any existing content.if (!shouldHydrate) {let warned = false;let rootSibling;while ((rootSibling = container.lastChild)) {// ...container.removeChild(rootSibling);}}return createLegacyRoot(container,shouldHydrate? {hydrate: true,}: undefined,); }
createLegacyRoot
定義于 ./ReactDOMRoot
中,指定了創(chuàng)建的DOM容器和一些option設置,最終會返回一個 ReactDOMBlockingRoot
。
export function createLegacyRoot( container: DOMContainer,options?: RootOptions, ): RootType {return new ReactDOMBlockingRoot(container, LegacyRoot, options); } function ReactDOMBlockingRoot( container: DOMContainer,tag: RootTag,options: void | RootOptions, ) {this._internalRoot = createRootImpl(container, tag, options); } function createRootImpl( container: DOMContainer,tag: RootTag,options: void | RootOptions, ) {// Tag is either LegacyRoot or Concurrent Root// ...const root = createContainer(container, tag, hydrate, hydrationCallbacks);// ...return root; }
關鍵點在于,方法最終調(diào)用了 createContainer
來創(chuàng)建root,而該方法中會創(chuàng)建我們上一節(jié)所介紹的 FiberRoot
,該對象在后續(xù)的更新調(diào)度過程中起著非常重要的作用,到更新調(diào)度內(nèi)容我們詳細介紹。
在這部分我們看到了兩個方法,分別是:createContainer、 updateContainer,均出自 react-reconciler/inline.dom
, 最終定義在 ``react-reconciler/src/ReactFiberReconciler` 。創(chuàng)建方法很簡單,如下
export function createContainer(containerInfo: Container,tag: RootTag,hydrate: boolean,hydrationCallbacks: null | SuspenseHydrationCallbacks,): OpaqueRoot {return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); }
我們繼續(xù)往下看,緊跟著能看到 updateContainer
方法,該方法定義了更新相關的操作,其中最重要的一個點就是 expirationTime ,直接譯為中文是過期時間,我們想想,此處為何要過期時間,這個過期的含義是什么呢?這個過期時間是如何計算的呢?繼續(xù)往下我們可以看到,computeExpirationForFiber
方法用于過期時間的計算,我們先將源碼片段放在此處。
export function updateContainer( element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function, ): ExpirationTime {const current = container.current;const currentTime = requestCurrentTimeForUpdate();// ...const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,current,suspenseConfig,);// ...const context = getContextForSubtree(parentComponent);if (container.context === null) {container.context = context;} else {container.pendingContext = context;}// ...const update = createUpdate(expirationTime, suspenseConfig);// Caution: React DevTools currently depends on this property// being called "element".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;}enqueueUpdate(current, update);scheduleWork(current, expirationTime);return expirationTime; }
計算完更新超時時間,而后創(chuàng)建更新對象 createUpdate
,進而將element綁定到update對象上,如果存在回調(diào)函數(shù),則將回調(diào)函數(shù)也綁定到update對象上。update對象創(chuàng)建完成,將update添加到UpdateQueue中,關于update和UpdateQueue數(shù)據(jù)結(jié)構見上一節(jié)講解。至此,開始任務調(diào)度。
setState 與 forceUpdate
這兩個方法綁定在我們當初定義React的文件中,具體定義在 react/src/ReactBaseClasses
中,如下
Component.prototype.setState = function(partialState, callback) {// ...this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; Component.prototype.forceUpdate = function(callback) {this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); };
這就是為何React基礎上拓展React-Native能輕松自如,因為React只是做了一些規(guī)范和結(jié)構設定,具體實現(xiàn)是在React-Dom或React-Native中,如此達到了平臺適配性。
Class組件的更新使用 this.setState
,這個api我們早已爛熟于心,對于對象組件的更新創(chuàng)建,定義在 react-reconciler/src/ReactFiberClassComponent.js
,classComponentUpdater對象定義了 enqueueSetState
與 enqueueReplaceState
以及 enqueueForceUpdate
對象方法,觀察這兩個方法會發(fā)現(xiàn),不同在于enqueueReplaceState
和 enqueueForceUpdate
會在創(chuàng)建的update對象綁定一個tag,用于標志更新的類型是 ReplaceState
還是 ForceUpdate
,具體實現(xiàn)我們一起來看代碼片段。
const classComponentUpdater = {isMounted,enqueueSetState(inst, payload, callback) {const fiber = getInstance(inst);const currentTime = requestCurrentTimeForUpdate();const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);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) {const fiber = getInstance(inst);const currentTime = requestCurrentTimeForUpdate();const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update = createUpdate(expirationTime, suspenseConfig);update.tag = ReplaceState;update.payload = payload;if (callback !== undefined && callback !== null) {// ...update.callback = callback;}enqueueUpdate(fiber, update);scheduleWork(fiber, expirationTime);},enqueueForceUpdate(inst, callback) {const fiber = getInstance(inst);const currentTime = requestCurrentTimeForUpdate();const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update = createUpdate(expirationTime, suspenseConfig);update.tag = ForceUpdate;if (callback !== undefined && callback !== null) {// ...update.callback = callback;}enqueueUpdate(fiber, update);scheduleWork(fiber, expirationTime);}, };
我們也能發(fā)現(xiàn),其實通過setState更新的操作實現(xiàn)和ReactDOM.render基本一致。
1.更新過期時間
2.創(chuàng)建Update對象
3.為update對象綁定一些屬性,比如 tag
、callback
4.創(chuàng)建的update對象入隊 (enqueueUpdate)
5.進入調(diào)度過程
expirationTime的作用
expirationTime用于React在調(diào)度和渲染過程,優(yōu)先級判斷,針對不同的操作,有不同響應優(yōu)先級,這時我們通過 currentTime: ExpirationTime
變量與預定義的優(yōu)先級EXPIRATION常量計算得出expirationTime。難道currentTime如我們平時糟糕代碼中的 Date.now()
?錯!如此操作會產(chǎn)生頻繁計算導致性能降低,因此我們定義currentTime的計算規(guī)則。
獲取currentTime
export function requestCurrentTimeForUpdate() {if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {// We're inside React, so it's fine to read the actual time.return msToExpirationTime(now());}// We're not inside React, so we may be in the middle of a browser event.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.currentEventTime = msToExpirationTime(now());return currentEventTime; }
該方法定義了如何去獲得當前時間,now
方法由 ./SchedulerWithReactIntegration
提供,對于now方法的定義似乎不太好找,我們通過斷點調(diào)試 Scheduler_now
,最終能夠發(fā)現(xiàn)時間的獲取是通過 window.performance.now()
, 緊接著找尋到 msToExpirationTime
定義在 ReactFiberExpirationTime.js
,定義了expirationTime相關處理邏輯。
不同的expirationTime
閱讀到 react-reconciler/src/ReactFilberExpirationTime
,對于expirationTime的計算有三個不同方法,分別為:computeAsyncExpiration
、computeSuspenseExpiration
、computeInteractiveExpiration
。這三個方法均接收三個參數(shù),第一個參數(shù)均為以上獲取的 currentTime
,第二個參數(shù)為約定的超時時間,第三個參數(shù)與批量更新的粒度有關。
export const Sync = MAX_SIGNED_31_BIT_INT; export const Batched = Sync - 1; const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = Batched - 1; // 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 MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0); } export function expirationTimeToMs(expirationTime: ExpirationTime): number {return (MAGIC_NUMBER_OFFSET - expirationTime) * 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(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,bucketSizeMs / UNIT_SIZE,)); } // TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update // the names to reflect. 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,); } export function computeSuspenseExpiration( currentTime: ExpirationTime,timeoutMs: number, ): ExpirationTime {// TODO: Should we warn if timeoutMs is lower than the normal pri expiration time?return computeExpirationBucket(currentTime,timeoutMs,LOW_PRIORITY_BATCH_SIZE,); } 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,); }
重點在于 ceil
方法的定義,方法傳遞兩個參數(shù),需要求值的number和期望精度precision,不妨隨意帶入兩個值觀察該函數(shù)的作用,number = 100,precision = 10,那么函數(shù)返回值為 (((100 / 10) | 0) + 1) * 10,我們保持precision值不變,更改number會發(fā)現(xiàn),當我們的值在100-110之間時,該函數(shù)返回的值相同。哦!此時恍然大悟,原來這個方法就是保證在同一個bucket中的更新獲取到相同的過期時間 expirationTime
,就能夠?qū)崿F(xiàn)在較短時間間隔內(nèi)的更新創(chuàng)建能夠__合并處理__。
關于超時時間的處理是很復雜的,除了我們看到的 expirationTime
,還有 childExpirationTime
、root.firstPendingTime
、root.lastExpiredTime
、root.firstSuspendedTime
、root.lastSuspendedTime
,root下的相關屬性標記了其下子節(jié)點fiber的expirationTime的次序,構成處理優(yōu)先級的次序,childExpirationTime
則是在遍歷子樹時,更新其 childExpirationTime
值為子節(jié)點 expirationTime
。
以上是React創(chuàng)建更新的核心流程,任務調(diào)度我們下次再見。
到此這篇關于React深入分析更新的創(chuàng)建源碼的文章就介紹到這了,更多相關React更新的創(chuàng)建內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
React-Router v6實現(xiàn)頁面級按鈕權限示例詳解
這篇文章主要介紹了使用 reac+reactRouter來實現(xiàn)頁面級的按鈕權限功能,這篇文章分三部分,實現(xiàn)思路、代碼實現(xiàn)、踩坑記錄,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-10-10React如何使用axios請求數(shù)據(jù)并把數(shù)據(jù)渲染到組件
這篇文章主要介紹了React如何使用axios請求數(shù)據(jù)并把數(shù)據(jù)渲染到組件,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08使用react-native-doc-viewer實現(xiàn)文檔預覽
這篇文章主要介紹了使用react-native-doc-viewer實現(xiàn)文檔預覽,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09react實現(xiàn)antd線上主題動態(tài)切換功能
這篇文章主要介紹了react實現(xiàn)antd線上主題動態(tài)切換功能,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08記錄React使用connect后,ref.current為null問題及解決
記錄React使用connect后,ref.current為null問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05