React原始碼分析(一)-呼叫ReactDOM.render後發生了什麼
所謂知其然還要知其所以然. 本系列文章將分析 React 15-stable的部分原始碼, 包括元件初始渲染的過程、元件更新的過程等. 這篇文章先介紹元件初始渲染的過程的幾個重要概念, 包括大致過程、建立元素、例項化元件、事務、批量更新策略等. 在這之前, 假設讀者已經:
- 對React有一定了解
- 知道React element、component、class區別
- 瞭解生命週期、事務、批量更新、virtual DOM大致概念等
如何分析 React 原始碼
程式碼架構預覽
首先, 我們找到React在Github上的地址, 把15-stable版本的原始碼copy下來, 觀察它的整體架構, 這裡首先閱讀關於原始碼介紹的
我們 要分析的原始碼在 src 目錄下:
|
|
分析方法
1、首先看一些網上分析的文章, 對重點部分的原始碼有個印象, 知道一些關鍵詞意思, 避免在無關的程式碼上迷惑、耗費時間;
2、準備一個demo, 無任何功能程式碼, 只安裝react,react-dom, Babel轉義包, 避免分析無關程式碼;
3、打debugger; 利用Chrome devtool一步一步走, 打斷點, 看呼叫棧,看函式返回值, 看作用域變數值;
4、利用編輯器查詢程式碼、閱讀程式碼等
正文
我們知道, 對於一般的React 應用, 瀏覽器會首先執行程式碼 ReactDOM.render
來渲染頂層元件, 在這個過程中遞迴渲染巢狀的子元件, 最終所有元件被插入到DOM中. 我們來看看
呼叫ReactDOM.render 發生了什麼
大致過程(只展示主要的函式呼叫):
如果看不清這有向量圖
讓我們來分析一下具體過程:
1、建立元素
首先, 對於你寫的jsx, Babel會把這種語法糖轉義成這樣:
|
|
沒錯, 就是呼叫React.createElement
來建立元素. 元素是什麼? 元素只是一個物件描述了DOM樹, 它像這樣:
|
|
React.createElement
原始碼在ReactElement.js
中, 其他邏輯比較簡單, 值得說的是props屬性, 這個props屬性裡面包含的就是我們給元件傳的各種屬性:
|
|
2、建立對應型別的React元件
創建出來的元素被當作引數和指定的 DOM container 一起傳進ReactDOM.render
. 接下來會呼叫一些內部方法, 接著呼叫了 instantiateReactComponent
, 這個函式根據element的型別例項化對應的component. 當element的型別為:
- string時, 說明是文字, 建立
ReactDOMTextComponent
; - ReactElement時, 說明是react元素, 進一步判斷element.type的型別, 當為
- string時, 為DOM原生節點, 建立
ReactDOMComponent
; - 函式或類時, 為react 元件, 建立
ReactCompositeComponent
- string時, 為DOM原生節點, 建立
instantiateReactComponent
函式在instantiateReactComponent.js :
|
|
3、開啟批量更新以應對可能的setState
在呼叫instantiateReactComponent
拿到元件例項後, React 接著呼叫了batchingStrategy.batchedUpdates
並將元件例項當作引數執行批量更新(首次渲染為批量插入).
批量更新是一種優化策略, 避免重複渲染, 在很多框架都存在這種機制. 其實現要點是要弄清楚何時儲存更新, 何時批量更新.
在React中, 批量更新受batchingStrategy
控制,而這個策略除了server端都是ReactDefaultBatchingStrategy
:
不信你看, 在ReactUpdates.js中 :
|
|
在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy
:
|
|
那麼React是如何實現批量更新的? 在ReactDefaultBatchingStrategy.js我們看到, 它的實現依靠了事務.
3.1 我們先介紹一下事務.
在 Transaction.js中, React 介紹了事務:
|
|
React 把要呼叫的函式封裝一層wrapper, 這個wrapper一般是一個物件, 裡面有initialize方法, 在呼叫函式前呼叫;有close方法, 在函式執行後呼叫. 這樣封裝的目的是為了, 在要呼叫的函式執行前後某些不變性約束條件(invariant)仍然成立.
這裡的不變性約束條件(invariant), 我把它理解為 “真命題”, 因此前面那句話意思就是, 函式呼叫前後某些規則仍然成立. 比如, 在調和(reconciliation)前後保留UI元件一些狀態.
React 中, 事務就像一個黑盒, 函式在這個黑盒裡被執行, 執行前後某些規則仍然成立, 即使函式報錯. 事務提供了函式執行的一個安全環境.
繼續看Transaction.js對事務的抽象實現:
|
|
這只是React事務的抽象實現(基類), 還需要例項化事務並對其加強的配合, 才能發揮事務的真正作用. 另外, 在React 中, 一個事務裡開啟另一個事務很普遍, 這說明事務是有粒度大小的, 就像程序和執行緒一樣.
3.2 批量更新依靠了事務
剛講到, 在React中, 批量更新受batchingStrategy
控制,而這個策略除了server端都是ReactDefaultBatchingStrategy
, 而在ReactDefaultBatchingStrategy.js中, 批量更新的實現依靠了事務:
ReactDefaultBatchingStrategy.js :
|
|
那麼, 為什麼批量更新的實現依靠了事務呢? 還記得實現批量更新的兩個要點嗎?
- 何時儲存更新
- 何時批處理
對於這兩個問題, React 在執行事務時呼叫wrappers的initialize方法, 建立更新佇列, 然後執行函式, 接著 :
- 何時儲存更新—— 在執行函式時遇到更新請求就存到這個佇列中
- 何時批處理—— 函式執行後呼叫wrappers的close方法, 在close方法中呼叫批量處理函式
口說無憑, 得有證據. 我們拿ReactDOM.render
會呼叫的事務ReactReconcileTransaction
來看看是不是這樣:
ReactReconcileTransaction.js 裡有個wrapper, 它是這樣定義的(英文是官方註釋) :
|
|
我們再看ReactReconcileTransaction
事務會執行的函式mountComponent
, 它在
ReactCompositeComponent.js :
|
|
而上述wrapper定義的close方法呼叫的this.reactMountReady.notifyAll()
在這
CallbackQueue.js :
|
|
即證.
你竟然讀到這了
好累(笑哭), 先寫到這吧. 我本來還想一篇文章就把元件初始渲染的過程和元件更新的過程講完, 現在看來要分開講了… React 細節太多了, 蘊含的資訊量也很大…說博大精深一點不誇張…向React的作者們以及社群的人們致敬!
我覺得讀原始碼是一件很費力但是非常值得的事情. 剛開始讀的時候一點頭緒也沒有, 不知道它是什麼樣的過程, 不知道為什麼要這麼寫, 有時候還會因為斷點沒打好繞了很多彎路…也是硬著頭皮一遍一遍看, 結合網上的文章, 就這樣雲裡霧裡的慢慢摸索, 不斷更正自己的認知.後來看多了, 就經常會有大徹大悟的感覺, 零碎的認知開始連通起來, 逐漸摸清了來龍去脈.
現在覺得確實很值得, 自己學到了不少. 看原始碼的過程就感覺是跟作者們交流討論一樣, 思想在碰撞! 強烈推薦前端的同學們閱讀React原始碼, 大神們智慧的結晶!
未完待續…