剖析 React 源碼:render 流程(二)

這是個人剖析 React 源碼的第三篇文章,若是你沒有閱讀過以前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。前端

文章相關資料

此篇文章內容銜接 render 流程(一),固然不看上一篇文章也沒什麼問題,由於內容並無強相關。react

如今請你們打開 個人代碼 並定位到 react-dom 文件夾下的 src 中的 ReactDOM.js 文件,今天的內容會從這裏開始。git

ReactRoot.prototype.render

在上一篇文章中,咱們介紹了當 ReactDom.render 執行時,內部會首先判斷是否已經存在 root,沒有的話會去建立一個 root。在今天的文章中,咱們將會了解到存在 root 之後會發生什麼事情。github

你們能夠先定位到代碼的第 592 行。性能優化

你們能夠看到,在上述的代碼中調用了 unbatchedUpdates 函數,這個函數涉及到的知識其實在 React 中至關重要。dom

你們都知道多個 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

接下來咱們來假定一些變量值,代入公式來算的話會更方便你們理解。

假如 originalStartTimeMs2500,當前時間爲 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 流程相關的內容。

最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。

相關文章
相關標籤/搜索