摘要:就是,如果你不了解這個的話可以閱讀下相關文檔,是應用初始化時就會生成的一個變量,值也是,并且這個值不會在后期再被改變。
這是我的剖析 React 源碼的第三篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。
文章相關資料React 16.8.6 源碼中文注釋,這個鏈接是文章的核心,文中的具體代碼及代碼行數都是依托于這個倉庫
熱身篇
render 流程(一)
此篇文章內容銜接 render 流程(一),當然不看上一篇文章也沒什么問題,因為內容并沒有強相關。
現在請大家打開 我的代碼 并定位到 react-dom 文件夾下的 src 中的 ReactDOM.js 文件,今天的內容會從這里開始。
ReactRoot.prototype.render在上一篇文章中,我們介紹了當 ReactDom.render 執行時,內部會首先判斷是否已經存在 root,沒有的話會去創建一個 root。在今天的文章中,我們將會了解到存在 root 以后會發生什么事情。
大家可以先定位到代碼的第 592 行。
大家可以看到,在上述的代碼中調用了 unbatchedUpdates 函數,這個函數涉及到的知識其實在 React 中相當重要。
大家都知道多個 setState 一起執行,并不會觸發 React 的多次渲染。
// 雖然 age 會變成 3,但不會觸發 3 次渲染
this.setState({ age: 1 })
this.setState({ age: 2 })
this.setState({ age: 3 })
這是因為內部會將這個三次 setState 優化為一次更新,術語是批量更新(batchedUpdate),我們在后續的內容中也能看到內部是如何處理批量更新的。
對于 root 來說其實沒必要去批量更新,所以這里調用了 unbatchedUpdates 函數來告知內部不需要批量更新。
然后在 unbatchedUpdates 回調內部判斷是否存在 parentComponent。這一步我們可以假定不會存在 parentComponent,因為很少有人會在 root 外部加上 context 組件。不存在 parentComponent 的話就會執行 root.render(children, callback),這里的 render 指的是 ReactRoot.prototype.render。
在 render 函數內部我們首先取出 root,這里的 root 指的是 FiberRoot,如果你想了解 FiberRoot 相關的內容可以閱讀 上一篇文章。然后創建了 ReactWork 的實例,這塊內容我們沒有必要深究,功能就是為了在組件渲染或更新后把所有傳入 ReactDom.render 中的回調函數全部執行一遍。
接下來我們來看 updateContainer 內部是怎么樣的。
我們先從 FiberRoot 的 current 屬性中取出它的 fiber 對象,然后計算了兩個時間。這兩個時間在 React 中相當重要,因此我們需要多帶帶用一小節去學習它們。
時間首先是 currentTime,在 requestCurrentTime 函數內部計算時間的最核心函數是 recomputeCurrentRendererTime。
function recomputeCurrentRendererTime() {
const currentTimeMs = now() - originalStartTimeMs;
currentRendererTime = msToExpirationTime(currentTimeMs);
}
now() 就是 performance.now(),如果你不了解這個 API 的話可以閱讀下 相關文檔,originalStartTimeMs 是 React 應用初始化時就會生成的一個變量,值也是 performance.now(),并且這個值不會在后期再被改變。那么這兩個值相減以后,得到的結果也就是現在離 React 應用初始化時經過了多少時間。
然后我們需要把計算出來的值再通過一個公式算一遍,這里的 | 0 作用是取整數,也就是說 11 / 10 | 0 = 1
接下來我們來假定一些變量值,代入公式來算的話會更方便大家理解。
假如 originalStartTimeMs 為 2500,當前時間為 5000,那么算出來的差值就是 2500,也就是說當前距離 React 應用初始化已經過去了 2500 毫秒,最后通過公式得出的結果為:
currentTime = 1073741822 - ((2500 / 10) | 0) = 1073741572
接下來是計算 expirationTime,這個時間和優先級有關,值越大,優先級越高。并且同步是優先級最高的,它的值為 1073741823,也就是之前我們看到的常量 MAGIC_NUMBER_OFFSET 加一。
在 computeExpirationForFiber 函數中存在很多分支,但是計算的核心就只有三行代碼,分別是:
// 同步
expirationTime = Sync
// 交互事件,優先級較高
expirationTime = computeInteractiveExpiration(currentTime)
// 異步,優先級較低
expirationTime = computeAsyncExpiration(currentTime)
接下來我們就來分析 computeInteractiveExpiration 函數內部是如何計算時間的,當然 computeAsyncExpiration 計算時間的方式也是相同的,無非更換了兩個變量。
以上這些代碼其實就是公式,我們把具體的值代入就能算出結果了。
time = 1073741822 - ((((1073741822 - 1073741572 + 15) / 10) | 0) + 1) * 10 = 1073741552
另外在 ceiling 函數中的 1 * bucketSizeMs / UNIT_SIZE 是為了抹平一段時間內的時間差,在抹平的時間差內不管有多少個任務需要執行,他們的過期時間都是同一個,這也算是一個性能優化,幫助渲染頁面行為節流。
最后其實我們這個計算出來的 expirationTime 是可以反推出另外一個時間的:
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}
如果我們將之前計算出來的 expirationTime 代入以上代碼,得出的結果如下:
(1073741822 - 1073741552) * 10 = 2700
這個時間其實和我們之前在上文中計算出來的 2500 毫秒差值很接近。因為 expirationTime 指的就是一個任務的過期時間,React 根據任務的優先級和當前時間來計算出一個任務的執行截止時間。只要這個值比當前時間大就可以一直讓 React 延后這個任務的執行,以便讓更高優先級的任務執行,但是一旦過了任務的截止時間,就必須讓這個任務馬上執行。
這部分的內容一直在算來算去,看起來可能有點頭疼。當然如果你嫌麻煩,只需要記住任務的過期時間是通過當前時間加上一個常量(任務優先級不同常量不同)計算出來的。
另外其實你還可以在后面的代碼中看到更加直觀且簡單的計算過期時間的方式,但是目前那部分代碼還沒有被使用起來。
scheduleRootUpdate當我們計算出時間以后就會調用 updateContainerAtExpirationTime,這個函數其實沒有什么好解析的,我們直接進入 scheduleRootUpdate 函數就好。
首先我們會創建一個 update,這個對象和 setState 息息相關
// update 對象的內部屬性
expirationTime: expirationTime,
tag: UpdateState,
// setState 的第一二個參數
payload: null,
callback: null,
// 用于在隊列中找到下一個節點
next: null,
nextEffect: null,
對于 update 對象內部的屬性來說,我們需要重點關注的是 next 屬性。因為 update 其實就是一個隊列中的節點,這個屬性可以用于幫助我們尋找下一個 update。對于批量更新來說,我們可能會創建多個 update,因此我們需要將這些 update 串聯并存儲起來,在必要的時候拿出來用于更新 state。
在 render 的過程中其實也是一次更新的操作,但是我們并沒有 setState,因此就把 payload 賦值為 {element} 了。
接下來我們將 callback 賦值給 update 的屬性,這里的 callback 還是 ReactDom.render 的第二個參數。
然后我們將剛才創建出來的 update 對象插入隊列中,enqueueUpdate 函數內部分支較多且代碼簡單,這里就不再貼出代碼了,有興趣的可以自行閱讀。函數核心作用就是創建或者獲取一個隊列,然后把 update 對象入隊。
最后調用 scheduleWork 函數,這里開始就是調度相關的內容,這部分內容我們將在下一篇文章中來詳細解析。
總結以上就是本文的全部內容了,這篇文章其實核心還是放在了計算時間上,因為這個時間和后面的調度息息相關,最后通過一張流程圖總結一下 render 流程兩篇文章的內容。
最后
閱讀源碼是一個很枯燥的過程,但是收益也是巨大的。如果你在閱讀的過程中有任何的問題,都歡迎你在評論區與我交流。
另外寫這系列是個很耗時的工程,需要維護代碼注釋,還得把文章寫得盡量讓讀者看懂,最后還得配上畫圖,如果你覺得文章看著還行,就請不要吝嗇你的點贊。
下一篇文章還是 render 流程相關的內容。
最后,覺得內容有幫助可以關注下我的公眾號 「前端真好玩」咯,會有很多好東西等著你。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/6881.html
摘要:大家可以看到是構造函數構造出來的,并且內部有一個對象,這個對象是本文接下來要重點介紹的對象,接下來我們就來一窺究竟吧。在構造函數內部就進行了一步操作,那就是創建了一個對象,并掛載到了上。下一篇文章還是流程相關的內容。這是我的剖析 React 源碼的第二篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。 文章相關資料 React ...
摘要:查看創建核心函數源碼行調用函數創建是相關,不用管源碼行這個指的是調用創建,下面我們將會說到對象源碼行源碼行函數中,首先創建了一個,然后又創建了一個,它們兩者還是相互引用。 感謝 yck: 剖析 React 源碼解析,本篇文章是在讀完他的文章的基礎上,將他的文章進行拆解和加工,加入我自己的一下理解和例子,便于大家理解。覺得yck寫的真的很棒 。React 版本為 16.8.6,關于源碼的...
摘要:我們先來看下這個函數的一些神奇用法對于上述代碼,也就是函數來說返回值是。不管你第二個參數的函數返回值是幾維嵌套數組,函數都能幫你攤平到一維數組,并且每次遍歷后返回的數組中的元素個數代表了同一個節點需要復制幾次。這是我的 React 源碼解讀課的第一篇文章,首先來說說為啥要寫這個系列文章: 現在工作中基本都用 React 了,由此想了解下內部原理 市面上 Vue 的源碼解讀數不勝數,但是反觀...
摘要:把組件看作狀態機有限狀態機使用來控制本地狀態使用來傳遞狀態前面我們探討了如何映射狀態到上初始渲染那么接下來我們談談時如何同步狀態到上的也就是是如何更新組件的是如何對比出頁面變化最小的部分這篇文章會為你解答這些問題在這之前你已經了解了版本內 React 把組件看作狀態機(有限狀態機), 使用state來控制本地狀態, 使用props來傳遞狀態. 前面我們探討了 React 如何映射狀態...
摘要:為了能夠更好的使用這個工具,今天就對它進行一下源碼剖析。它內部的關鍵代碼是在不指定的時候等于,這就意味著的源碼剖析到此結束,謝謝觀看當然如果指定了剖析就還得繼續。好了,源碼剖析到此結束,謝謝觀看 React-Redux是用在連接React和Redux上的。如果你想同時用這兩個框架,那么React-Redux基本就是必須的了。為了能夠更好的使用這個工具,今天就對它進行一下源碼剖析。 Pr...
閱讀 864·2021-11-19 11:29
閱讀 3357·2021-09-26 10:15
閱讀 2867·2021-09-22 10:02
閱讀 2442·2021-09-02 15:15
閱讀 1979·2019-08-30 15:56
閱讀 2415·2019-08-30 15:54
閱讀 2914·2019-08-29 16:59
閱讀 642·2019-08-29 16:20