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