當前前端三大框架(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的流程大致如此,源碼大概將近兩萬行,比較復雜,但是細分下去大致流程就是這樣的。