歡迎訂閱 React技術揭祕 代碼參照React 16.13.1 。
假設React是你平常開發的框架,在日復一日的開發中,你萌生了學習React源碼的念頭,在網上一頓搜索後,你發現這些教程能夠分爲2類:javascript
《xx行代碼帶你實現迷你React》,《xx行代碼實現React hook》這樣短小精幹的文章。若是你只是想花一點點時間瞭解下React的工做原理,我向你推薦 這篇文章,很是精彩。html
《React Fiber原理》,《React expirationTime原理》這樣摘錄React源碼講解的文章。若是你想學習React源碼,當你都不知道Fiber
是什麼,不知道expirationTime
對於React的意義時,這樣的文章會給人「你講解的代碼我看懂了,但這些代碼的做用是什麼」的感受。java
我要寫的這個系列文章和對應倉庫的存在就是爲了解決這個問題。react
簡單來講,這個系列文章會講解React爲何要這麼作,以及大致怎麼作,但不會有大段的代碼告訴你怎麼作。git
當你看完文章知道咱們要作什麼後,再來看倉庫中具體的代碼實現。github
同時爲了防止堆砌不少功能後,代碼量太大影響你理解某個功能的實現,我爲倉庫每一個功能的實現打了一個git tag
。web
RectDOM.render(<App/>, document.getElementById('app'));複製代碼
沒有state、沒有Hooks、沒有函數組件和類組件,只能渲染首屏元素,可是全部目錄架構、文件名、方法都和React同樣,代碼片斷徹底同樣(由於就是一邊debug一邊抄的)。npm
若是你想讀React源碼,但又被React龐大的代碼量勸退,我相信這個項目適合你起步。react-native
npm start複製代碼
這是這個系列第一篇文章,對應 git tag v1,正餐開始~數組
咱們知道,React是一個聲明式的UI庫,咱們經過組件的形式聲明UI,React會爲咱們輸出DOM並渲染到頁面上。
在React中,對UI的聲明是經過一種稱爲JSX的語法糖來實現。JSX在編譯時會被Babel轉換爲React.createElement方法。
在運行時咱們獲取到的實際上是
// 輸入JSX const a = <div>Hello</div> // 在編譯時,被babel編譯爲React.createElement函數 const a = React.createElement('div', null, 'Hello'); // 在運行時,執行函數,返回描述組件結構的對象 const a = { ?typeof: Symbol(react.element), "type": "div", "key": null, "props": { "children": "Hello" } }複製代碼
咱們能夠看到,在運行時描述組件結構的對象離渲染到頁面上的DOM還相去甚遠,爲了能渲染DOM到頁面上,React內部確定有2個模塊:
在React中,咱們把模塊1作的工做叫render,把模塊2作的工做叫commit。
爲何叫這個名字呢,想一想你寫的ClassComponent的render方法,在render階段要作的一件事就是執行render方法。
至於commit,可能你會想到 git commit 。事實上,React的工做流程和Git多分支開發很是類似。
因此,更新下咱們的架構:
到目前爲止,咱們簡單介紹了render和commit,有了這2個階段,咱們已經能夠實現除了異步模式(Concurrent)外React的大部分功能。
可是,設想如下場景:
甚至極端的考慮,咱們已經觸發了2,在計算2須要改變的DOM節點的過程當中用戶又觸發了1,這時候若是能擱置2轉而優先處理1,這種體驗是符合預期的。
因此咱們須要一種機制來處理更新的優先級,決定哪一個狀態變化帶來的更新應該被優先執行。
爲了達到這個目的,咱們知道須要爲現有架構增長一個schedule階段:
基於咱們如今的設計,commit階段負責把須要渲染的DOM元素渲染到頁面上。
可是React的野心歷來不只限於web端,理論上當render階段決定了哪些JSX須要被渲染後,咱們對應不一樣的commit,就能實如今不一樣平臺的渲染。
當咱們嘗試渲染 <App/> 時,在render階段會生成右側的Fiber結構。Fiber的完整結構看這裏。
在React中,咱們的組件會造成一棵組件樹,一樣的,有了Fiber的結構後,咱們須要將他們連接在一塊兒組成Fiber樹。咱們爲Fiber增長以下字段:
小朋友,此時你是否有不少???
爲啥這個字段叫return,不叫parent,React核心團隊的Andrew Clark解釋說:能夠理解爲return指向當前Fiber處理完後返回的那個Fiber,當子Fiber被處理完後會返回他的父Fiber。好吧
因此咱們的完整Fiber結構是這樣的:
如今咱們有了描述組件的節點類型(Fiber),能夠愉快的開始首屏渲染了。
須要注意的是,因爲執行ReactDOM.render產生的首屏渲染並不涉及到其餘更高優先級的更新,因此對於首屏渲染,咱們掠過schedule階段。
好比剛纔介紹schedule階段舉的地址輸入框的例子,首屏渲染了輸入框,更高優先級的更新是後續在輸入框中輸入文字產生的。
當咱們首次進入render階段時,咱們傳入JSX:
整個render階段須要作2件事:
PS:這裏同窗可能會奇怪,這一步爲何是「爲每一個節點的子節點生成對應的Fiber」而不是「爲當前節點生成對應的Fiber」?還記得下面這行代碼麼:
2. 爲每一個Fiber生成對應的DOM節點,保存在Fiber.stateNode
作完這2件過後咱們進入commit階段,此時咱們知道
有了這些信息,Commit階段只須要遍歷全部有Placement反作用的Fiber,依次執行DOM插入操做就完成了首屏的渲染。
這就是首屏渲染render+commit的整個過程。機智如你,是否是理解起來徹底沒壓力呢。
咱們剛纔講了render階段會作2件事(會調用的2個函數),如今咱們給他們起個名字吧:
向下遍歷JSX,爲每一個JSX節點的子JSX節點生成對應的Fiber,並設置effectTag
咱們叫他beginWork,這是每一個節點render階段開始工做的起點。
爲每一個Fiber生成對應的DOM節點
咱們叫他completeWork,這是每一個節點render階段完成工做的終點。
咱們經過workInProgress這個全局變量表示當前render階段正在處理的Fiber,當首屏渲染初始化時, workInProgress === 根Fiber。
調用workLoopSync方法,他內部會循環調用performUnitOfWork方法。
performUnitOfWork每次接收一個Fiber,調用beginWork或CompleteWork,處理完該Fiber後返回下一個須要處理的Fiber。
當performUnitOfWork返回null時,就表明全部節點的render階段結束了。
整個流程雖然看起來繁瑣,但就作了2件事:
在這個過程當中若是遇到兄弟節點,又重複步驟1,直到最終又回到根Fiber,完成整棵樹的建立與遍歷。
在咱們的設計中,commit階段會遍歷找到全部含有effectTag的Fiber節點。若是Fiber樹很龐大的話,這個遍歷會很耗時。
但其實在render階段咱們已經知道哪些Fiber會被設置Fiber.effectTag, 因此咱們能夠在render階段就提早標記好他們,將他們組織成鏈表的形式。
假設圖中標紅的Fiber表明本次調度該Fiber有effectTag,咱們用鏈表的指針將他們連接起來造成一條單向鏈表,這條鏈表就是 effectList。
用Redux做者Dan Abramov的話來講,effectList相對於Fiber樹,就像聖誕樹上的彩蛋
有了effectList,commit階段只須要遍歷這條鏈表就能知道全部有effectTag的Fiber了。這部分代碼在completeUnitOfWork函數中。
按照咱們的架構,咱們會給須要插入到DOM的Fiber賦值
fiber.effectTag = Placement;複製代碼
這對於某次增量更新來講沒有問題,但對於首屏渲染卻過低效了,畢竟對首屏渲染來講,全部Fiber節點對應的DOM節點都是須要渲染到頁面上的。
難道咱們要給全部Fiber賦值effectTag = Placement;再在commit階段一次次的執行DOM插入操做來生成一整棵DOM樹?對於首屏渲染,咱們須要稍微變通下。
當咱們在render階段執行completeWork建立Fiber對應的DOM節點時,咱們遍歷一下這個Fiber節點的全部子節點,將子節點的DOM節點插入到建立的DOM節點下。
(子Fiber的completeWork會先於父Fiber執行,因此當執行到父Fiber時,子Fiber必定存在對應的DOM節點)。代碼見這裏
這樣當遍歷到根Fiber節點時,咱們已經有一棵構建好的離屏DOM樹,這時候咱們只須要賦值根節點的effectTag就能在commit階段一次性將整課DOM樹掛載。
// 僅賦值根fiber一個節點effectTag
RootFiber.effectTag = Placement; 複製代碼
// 賦值根fiber
workInProgress = Rootfiber;複製代碼
這中間發生了什麼?
複習小課堂:workInProgress指當前render階段正在處理的Fiber,ReactDOM.render會建立一個RootFiber,他會賦值給workInProgress
{
// UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
tag: UpdateState,
// 更新的state
payload: null,
// 指向當前Fiber的下一個update
next: null
}複製代碼
調用React ClassComponent的this.setState,會產生一個update,update.payload爲須要更新的state,在該ClassComponent對應的Fiber執行beginWork時會處理state的更新帶來的組件狀態改變,固然,在V1版本咱們尚未實現。
對於調用ReactDOM.render使根Fiber初始化時,會產生一個update,update.payload爲對應須要渲染的JSX(代碼見這裏),在根Fiber的beginWork中會觸發這篇文章講到的render流程。
篇幅有限,咱們講的不少都是宏觀的東西,要了解細節還須要多多debug代碼,把咱們的Demo單步調試幾遍。
這裏再給你推薦一篇極好的React原理文章,配合本文食用效果極佳