React 框架的設計思想及源碼結構


當前前端三大框架(vue、react和angular),除了vue之外,國內用得最多的就是react了,之前一直對其實現原理比較好奇,在花了很多時間深入研究了其源碼實現后,本篇開始記錄一下

同樣的功能,用vue和react都能實現,相比較vue,react的學習門檻比較高,但是好處是它非常靈活,執行的效率更高(用到了很多新的技術),我個人覺得react的代碼和vue的代碼就像linux和windows,前者很注重javascript功底(類似linux的shell命令),后者有很多現成的html擴展標簽指令( v-for、v-if等,類似windows的圖形界面),所以如果一個人的js語言研究得比較深入,和一個剛剛入門js語言的程序員來說,用React實現了同樣的需求,敲出來的代碼質量會差很多的

react更加的純粹,這里的純粹指的是什么的,在react內部,jsx模板經babel轉化后是一個對象,所有的操作都是基於這個對象和其對應的fiber結構來操作的。

vue和react有許多共同點,比如:

  • 都使用了虛擬DOM
  • 更新時都使用了diff算法進行了優化

react和vue的不同之處如下

 writer by:大沙漠 QQ:22969969

  vue框架 react框架
實現原理 將模板轉化成一個render函數來執行 將每個節點轉化為fiber對象,最終形成一個fiber樹結構,來依次渲染
更新時的原理 通過ES5的Object.defineproperty()來動態觸發的 通過兩個fiber的對比來實現更新
是否支持雙向綁定 支持,使用v-model實現 不支持,需要手動配置
指定模板的位置  使用el指定DOM節點,或者template設置模板,又或者直接通過render來返回子節點VNode 在函數組件返回jsx,或者class組件的render方法內返回jsx
模板的格式 使用html標簽格式,只是加了幾個vue內置的標簽或者屬性 用jsx指定模板,jsx類似JavaScript的擴展
更新過程中可以否被打斷 不能被打斷 某些流程可以被打斷,讓優先級更高的任務優先執行(例如瀏覽器渲染)

舉個例子,看看實現同樣的功能,實現的效果是,在頁面上創建如下兩個DOM節點:

  • p節點     ;默認顯示hello world字符串
  • button              ;一個按鈕,點擊后會將helllo world變為hello vue或hello react

vue代碼如下:

<div id="app"><button @click="test">測試</button><p>{{str}}</p></div>         <!--Vue的寫法-->
<script>
    new Vue({
        el:"#app",
        data:{
            str:"hello world"
        },
        methods:{
            test(){
                this.str="hello vue";
            }
        }
    })
</script>

效果如下:

react代碼如下啊:

<div id="root"></div>                       <!--React的寫法-->
<script type="text/babel">
    class App extends React.Component{
        constructor(props){                
            super(props)
        }         
        state = {str:"hello world"}
        test = () => this.setState(val=>val.str="hello React")
        render(){
            return  <div>
                        <button onClick={this.test}>測試</button>
                        <p>{this.state.str}</p>
                    </div>
        }
    }
    ReactDOM.render(<App/>,root)
</script>

 效果如下:

整個React應用從初始化到結束可以分為四個步驟:

  • 創建更新     ;進行React的初始化工作,會將reactelement對象轉化為一個fiber對象,然后形成一個fiber樹的解構,之后的所有操作都是基於這個fiber樹來進行操作的
  • 異步調度               ;React中有同步任務、異步任務、這個過程用於處理異步任務當中的邏輯,當瀏覽器渲染完后有有空余時間時開始執行這個調度,也就是第三步的render階段
  • render階段    ;這個階段主要用於處理fiber樹的更新,所有的事件綁定、css設置、class中大部分的生命周期函數、context、ref等任務,絕大多數可以被中斷的任務都會在 這個階段執行,這個階段是可以被打斷的,最終會形成一個effect鏈,每個元素是一個fiber對象,供第四步使用
  • commit階段           ;主要遍歷第三步render階段生成的effect鏈,依次執行每個fiber元素上的收尾工作,這個階段是同步任務,不能被打斷的,因此大部分可以被中斷的任務都在第三步render階段執行完了

創建更新階段(初始化階段)

React用jsx來指定模板, jsx類似於JavaScript的擴展語法,經過babel轉化后會轉化為一個React.createElement函數,例子里的ReactDOM.render(<App/>,root)里的<App/>里的<App/>就是一個jxs對象,經過babel轉化后為:

React.createElement(React.createElement(App, null), null)

經過babel轉化后為如下函數:

React.createElement(
    "div", 
    null, 
    React.createElement("button", {onClick: (void 0).test}, "\u6D4B\u8BD5"), 
    React.createElement("p", null, (void 0).state.str)
);

React.createElement()函數執行后會返回一個React Element對象,這里返回的對象如下:

{
    $$typeof:Symbol(react.element), //表示當前對象是一個ReactElement對象  type:App, key:null, ref:null, props:{}, _owner:null

另外對於React里的類來說,render函數也需要返回jsx,比如例子里的render返回的如下

<div> <button onClick={this.test}>測試</button> <p>{this.state.str}</p> </div>

經過babel轉化后轉化為如下這個ReactElement對象

{
    $$typeof: Symbol(react.element),             //表示當前對象是一個ReactElement對象
    type: "div",                                 //組件的類型
    key: null,                                     //組件的key
    ref: null,                                     //組件的ref
    props:[                                     //組件的key
        {
            $$typeof: Symbol(react.element),
            type: "button",
            key: null,
            ref: null,
            props:{
                children:'測試',onClick:f
            },
            _owner:{...}                             
        },{
            $$typeof: Symbol(react.element),
            type: "p",
            key: null,
            ref: null,
            props:{
                children:"hello world",
            },
            _owner:{...}
        }
    ],
    _owner:{...}                                 //記錄負責創建此元素的組件,可以是class組件、function組件,或者null
}

回到ReactDOM.render(<App/>,root),經過babel轉化為ReactDOM.render(React.createElement(App, null),null),由於表達式是由內向外執行的,因此該函數會先執行React.createElement()函數將<App/>轉化為一個ReactElement對象后,然后再執行ReactDOM.render()函數,這樣就開始了ReactDOM的邏輯了。

在ReactDOM.render里會經過一些列的初始化,創建一個fiber樹,大致如下:

一般我們執行ReactDOM.render(<App/>,root)時在第一階段都會生成這樣的數據結構,之后React所有的操作都是基於這個fiber樹進行更新的,每個ReactDOM.render()都會生成一個ReactRoot對象,ReactRoot、FiberRoot都有其作用,RootFiber就是根節點Fiber對象了

App類對應的fiber在render的時候就會把它的render函數返回的jsx(也就是ReactElement對象)都轉化為fiber對象,具體后面再詳解

我們可以把每個fiber理解為一個DOM對象的映射,絕大多數DOM對象都有一個其對應的fiber對象的(文本節點是沒有fiber對象的,react直接通過nodeValue來設置了)。

執行完創建更新階段,之后就進入了React的任務調度截斷了

 

任務調度階段

React中的任務分為同步和異步任務,如果是同步任務則會直接跳過這個階段進入render階段,對於異步任務來說,它會在瀏覽器渲染完之后利用空余的時間進行更新。

任務調度會利用requestAnimationFrame這個原生的API接口,requestAnimationFrame函數會在每次瀏覽器前執行的,執行的時候react利用了postMessage函數在任務隊列里插入了一個函數,這樣等到瀏覽器重繪完成后就會執行這個任務了,然后會觸發相應的邏輯,執行第三步驟render階段的相關操作。

 

render階段

render階段會依次遍歷在第一步生成的fiber結構,利用深度優先遍歷的算法,先遍歷整個fiber樹最左側的fiber對象,然后再遍歷到右側的,最終回到最底層的根fiber對象,中間根據不同的組件類型做不同的處理,這個階段也是整個React最難理解的一個階段,因為有非常多的處理函數,還可以被高優先級的任務給打斷,例如瀏覽器重繪,自定義事件等。這個階段完成后在最頂層的fiber的firstEffect和lastEffect上設置一個鏈表,指向所有需要在commit階段進行處理的fiber

 

commit階段

這個階段比較簡單,就是遍歷第三步最后生成的Effect鏈,依次在每個fiber上執行收尾的工作。

 

React的流程大致如此,源碼大概將近兩萬行,比較復雜,但是細分下去大致流程就是這樣的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM