『壹』 React 源碼探源 1 Mount
React 在上一周(2021年6月8日) 出了 18 beta ,作為一個資深 React 玩家,如果未能通讀過 React 源碼,出門都不好意思跟人打招呼。本系列是筆者通讀 React 17 源碼的一個嘗試。
本系列並不是 React 官方作者的行為,有些結論是處於筆者自己的理解,如有不當或者遺漏請各位老師和同學指正。
本系列盡量在完整的主題上切分主題。然而因為 React 源碼的復雜度,這幾無可能。如果未能做到完整的切分,本文應盡量做到各個文章之間的無關性。
從頭自己來讀 React 的源碼是一件很有挑戰的事情,而且有可能誤入歧途。本文並不是從頭開始自己來,主要參考了 Youtube 的 React Source Code Walkthrough 。 需要的同學請自行帶梯子。
本文所有的源碼都參考於 react repo , 使用的版本是 17.0.3 , Commit 是
如要探尋 React 源碼,需要本地編譯啟動 React 源碼,請參考 React 官方文檔
一般來講,開發環境都是完備的,筆者只是安裝了 JDK 。
在開始之前,需要保證
首先我們打開 fixtures/packaging/babel-standalone/dev.html ,可以看到頁面的渲染結果:
一般來講,React 將整個 Mount 過程分解為 Render 和 Commit 過程,Render 的過程主要在 workLoopSync 完成,Commit 過程主要在 commitRoot 中完成。
實際上觀察調用棧可以發現,在 Render 之前, React 還有調用了 ,這個過程目前發現的主要工作是給 ReactDOM 掛載點初始化事件監聽,就是在 listenToAllSupportedEvents 中完成的工作。
本階段主要是創建 fiber tree,以及 fiber tree 對應的 DOM Tree,每個 fiber 對應於一個 work,對應一個 React 節點。
渲染的 fiber tree 可以在 ReactDOM 的 DOM 的根結點查看到:
本系列後續會通過更多的例子來探究 Fiber 樹的結構,現在可以先說一下現在筆者發現的一些結論。
Fiber 樹中使用 child 表述子節點,通過 return 表述父節點,通過 sibling 表述兄弟節點。通過 stateNode 表述關聯的 DOM 節點。
Fiber 樹實際上有兩個,相互之間通過 alternate 連接。在 commit 之前,fiber 樹根的 current 下面還沒有子節點,當整體渲染結束以後,fiber 樹根的 current 會指向當前的 current.alternate .
Commit 階段會將現在的 fiber tree 渲染到 DOM 樹中,並且執行組件中可能的 effect。本系列會用更多的例子來探究 Commit 的過程。
『貳』 create-react-app build 打包隱藏源碼
在使用 create-react-app 時,打包生產環境 npm run build ,瀏覽器打開後仍然是可以看到源碼的。
在這里以新建一個默認項目為例:
項目根目錄新建 .env.proction 文件,內容如下:
然後重新打包,瀏覽器打開後就看不到源碼啦。
為了探究原理,執行 eject 後,可以看到webpack配置中有這么一段
這里的 process.env.GENERATE_SOURCEMAP 控制著是否捎帶源碼。所以我們可以配置環境變數 GENERATE_SOURCEMAP=false 即可。
當執行 build 時,將按順序優先尋找 .env.proction.local , .env.proction , .env.local , .env 文件來配置環境變數,所以就有了上面的操作。
更多關於環境變數的信息可查看 Adding Custom Environment Variables 。
『叄』 【原創】react-源碼解析 - forward-ref&context(4)
通常我們會通過ref去獲取Dom節點的實例,或者ClassComponent的實例,但是,如果我么們的組件是一個function類型的component,由於functionComponent是沒有實例的所以我們在使用的時候也相應的取不到改組件的this,當然ref也一樣。這時react為我們提供了一個forwardRef方法:
通過這種方式創建的函數類型的組件,使我們能夠在函數中繼續使用ref屬性,當然我們在實際的應用的中也不會傻到去取函數類型組件的ref,因為我們知道它是沒有實例的。但是,當我們在使用其他庫提供的組件的時候,我們可能並不知道這個這個組件的類型,這時如果能夠合理的使用這個方法將會為我們省去不必要的麻煩,同時這里也有HOC的思想在裡面。接收一個組件,返回對原組件進行包裝的新的組件。接下來我們去看看方法的源碼: forwardRef 源碼很簡單,改方法返回了一個Oject具有render屬性,同時$$typeof為"react.forward_ref"的Symbol值。
這里可能存在對於type屬性的概念混淆。我們一定不能認為使用forward創建出的組件的$$typeof屬性為:'react.forward_ref'。我們使用forwardRef創建的組建的額時候,實際是將上面例子中的TargetCom作為參數傳入到CreateElement方法中的,實際返回的element中的$$typeof還是REACT_ELEMENT_TYPE, 也就是說這里我們將TargetCom{創建出的對象--具有render和$$typeof屬性}傳入,其中CreateElement的type屬性為forward方法返回的那個對象,也就是說在type對象裡面有個叫做$$typeof的屬性這個屬性的鍵值為:'react.forward_ref',
在後安的渲染過程中有很多判斷,其中有一些就是更具$$typeof展開的,這里我們一定要搞清楚凡是通過CreateElement創建的組件的$$typeof屬性都為: 'REACT_ELEMENT_TYPE'。
這里我們還是按照慣例對api進行一下簡單的說明,我們知道在react中是通過props屬性來實現組件間通信的,這種通信方式存在的問題在於,雖然父子組件之間通信很方便但是當我們的組件嵌套層級很深,這時候如果使用props傳參就不太現實了,首先中間層的組件不一定是你自己寫的其次中間層組件聲明的props對於這些組件本身沒有任何意義,這個時候我們就需要使用context方法幫助我們實現多級組件之間的通信。我們在頂層組件中提供了context對象之後,所有的後代組件都可以訪問這個對象。以此達到跨越多層組件傳遞參數的功能。在react當前版本中有兩種實現context的方式:
(1)ParentComponent.childContextTypes{} == {不推薦,下個大版本會廢棄}
(2)const { Provider, Consumer } = React.createContext('default');
在使用childContextTypes時候我們需要在父級組件中聲明一個getChildContext的方法,該方法返回一個對象,這個對象就是我們需要傳給後代組件的context對象。當我們在使用第一種方法的時候我們需要在組件上聲明context對象中屬性的類型,有些類似於react的PropTypes類型檢測。同時需要在使用到context的後代組件中聲明contextTypes類似於下面這種寫法:
如果不這樣聲明的話,在後代組價中是取不到context對象的。這里我們需要注意的是我們在子組件中使用context的時候,需要哪個屬性就必須去contextTypes中聲明,因為改組件的上級組件不止一個上級組件中的context也不止一個。而createContext方法的使用就簡化了很多,首先我們看到改方法返回兩個對象Provider, Consumer分別為context的提供方和訂閱方:
在上層組件中聲明之後,在想用到context的後代組件中國使用Consumer包括起來就可以訪問到之前聲明的context: ReactContext
從源碼中我們可以看到CreateContext方法創建了一個對象改對象有一個_currenValue屬性記錄context的變化,這個對象Provider屬性中聲明context,然後使改對象的Consumer屬性指向對象本身,我們在使用Consumer的時候就直接從context的currenValue上去取值。以上就是react中的Createcontext方法的實現原理,當然實際過程並沒有這么簡單,至於具體的實現我們接著往下看。同時這里我們也需要注意該對象下的$$typeof屬性並不是用來替換ReactElement中的$$typeof, 與我們之前將到的forwardRef中聲明的$$typeof一樣都只是我們傳入CreateElement方法中type屬性上的內容。
了解更多: react-source-code
『肆』 深入理解React16之:(一).Fiber架構
React16雖然出了一陣子了。剛出來的時候,粗略看了一遍更新文檔。以為沒什麼大的改動,也聽說項目從react15-16的升級過度可以很平滑,再加上項目改版上線一直比較頻繁,所以一直還用的15.6的版本。
偶然在知乎看到@程墨Morgan大神的live,便抱著好奇心和學習的心態報名了,受益良多。
我理解的Fiber架構:
在我之前的一篇文章有簡單介紹, 閱讀react源碼--記錄:1.1 問題記錄
下面從一個具體實例理解一下,再加上我畫了圖,應該很好理解啦~(圖畫的有點渣)
假如有A,B,C,D組件,層級結構為:
我們知道組件的生命周期為:
掛載階段:
那麼在掛載階段,A,B,C,D的生命周期渲染順序是如何的呢?
以render()函數為分界線。從頂層組件開始,一直往下,直至最底層子組件。然後再往上。
組件update階段同理。
————————
前面是react16以前的組建渲染方式。這就存在一個問題,
好似一個潛水員,當它一頭扎進水裡,就要往最底層一直游,直到找到最底層的組件,然後他再上岸。在這期間, 岸上發生的任何事,都不能對他進行干擾,如果有更重要的事情需要他去做(如用戶操作),也必須得等他上岸
看一下fiber架構 組建的渲染順序
潛水員會每隔一段時間就上岸,看是否有更重要的事情要做。
加入fiber的react將組件更新分為兩個時期
這兩個時期以render為分界,
phase1的生命周期是可以被打斷的,每隔一段時間它會跳出當前渲染進程,去確定是否有其他更重要的任務。此過程,React 在 workingProgressTree (並不是真實的virtualDomTree)上復用 current 上的 Fiber 數據結構來一步地(通過requestIdleCallback)來構建新的 tree,標記處需要更新的節點,放入隊列中。
phase2的生命周期是不可被打斷的,React 將其所有的變更一次性更新到DOM上。
這里最重要的是phase1這是時期所做的事。因此我們需要具體了解phase1的機制。
這樣的話,就和react16版本之前有很大區別了,因為可能會被執行多次,那麼我們最好就得保證phase1的生命周期 每一次執行的結果都是一樣的 ,否則就會有問題,因此, 最好都是純函數。
對了,程墨大神還提到一個問題,飢餓問題,即如果高優先順序的任務一直存在,那麼低優先順序的任務則永遠無法進行,組件永遠無法繼續渲染。這個問題facebook目前好像還沒解決,但以後會解決~
所以,facebook在react16增加fiber結構,其實並不是為了減少組件的渲染時間,事實上也並不會減少,最重要的是現在可以使得一些更高優先順序的任務,如用戶的操作能夠優先執行,提高用戶的體驗,至少用戶不會感覺到卡頓~
『伍』 React 源碼(三)使用本地依賴庫
在 React 應用中依賴基本上是通過 yarn 或者 npm 進行安裝的,但是在看源碼的過程中,有的時候想要去調試,或者說列印一些數據,如果可以在本地的 React 應用裡面依賴本地的 React 倉庫,那麼就可以進行上述的操作了。
在 React 官方文檔中的開發流程 裡面介紹了如何使用本地依賴庫。
在啟動本地 React 項目的時候出現了以下報錯
在將 react-jsx-dev-runtime.development.js 文件復制到 build/node_moles/react/cjs 目錄下即可。