本章將講述React
代碼庫的組織,約定,和它的實現方式。
如果你想更加關注Reac
t,或者說作爲開發貢獻者,對React
進行一些修改,這篇博客或許可以幫到你。
當然,我們沒必要去過度的關注React
應用的約定,因爲其中有很多是歷史遺留問題,後續版本可能會被pass
掉。
1.自定義模板系統
在Facebook
,他們內部人員使用了一個叫做Haste
的自定義模板系統,這個系統非常類似CommonJS
規範,也使用require()
,但是又有些不同從而讓部分開發者很尷尬,迷糊着。
在CommonJS
中,你使用相對路徑來導入一個模板
// 同一目錄下
var setInnerHTML = require('./setInnerHTML');
// 不同目錄下
var setInnerHTML = require('../utils/setInnerHTML');
// 多層
var setInnerHTML = require('../client/utils/setInnerHTML');
上述的setInnerHTML
可以在多個文件夾中存在,但是,在React
代碼庫中,您可以導入任何模板與其他模板的名稱,Haste
要求名稱必須是全局唯一的。
var setInnerHTML = require('setInnerHTML');
Haste
最開始被設計出來就是爲了開發大型項目的,比如Facebook
,你可以將你需要的文件放在不同的文件下面,導入時也不用使用什麼相對地址,通過一個全局唯一的名字,它可以幫你定位到文件,這也是Haste
的特點,文件名必須唯一,在同一個項目中不能同時出現兩個相同的文件名。
React
他自身就是從Facebook
這個大型項目的代碼庫分離出來的,所以它會保留一些Haste
的一些特性,在以後的版本,React
可能會使用CommonJS
或者ES6
的模板導入來代替它,當然,在Facebook
內部的話可能會很難發生改變,畢竟項目這麼大,基本架構都已成熟。
這是一些Haste
的規則:
在
React
源碼庫中文件名必須唯一,[ 這也是它的一些代碼會比較冗長的原因] 當你導入一個新的文件時,你的文件必須包含一個許可頭,你可以從已經存在的代碼庫中得到這個許可頭,這個許可頭類似於
@providesModule setInnerHTML
,github地址,在新文件中,你只要對應着修改就可以了。在導入時別使用相對路徑,不寫
require('./setInnerHTML')
,而寫require('setInnerHTML')
。
當我們使用npm
來處理React
時,一個腳本會複製所有的模塊到一個叫做lib
文件夾中,這種方式就是用require
加絕對或者相對地址來處理,在Nodejs
,browerify
,webpack
和其它一些工具都是這樣來處理React
模塊的,但是這和Haste
並沒有什麼關係。
2.外部依賴關係
React
一般都沒有什麼外部依賴,但是比如fbjs
,Relay
等等,雖然不是React
的公共API
,但是都是Facebook
內部分離出來的,不能算是一種外部依賴。
3.頂級文件夾
通過查看React
的庫,我們可以看到如下的文件結構:
src
是React
的源代碼文件夾,如果你要修改相關代碼,src
你可能需要花大部分時間去看看,學習學習docs
是React
文檔網頁夾,如果你改變了API
的話,就有必要更新相關文檔文件。examples
包含React demo
的一部分事例packages
包含一些在React
代碼庫中的代碼分佈結構(比如package.json
)。build
是React build
命令的輸出。
還有其他的文件夾,這裏就不多說了。
4.單元測試
一般都沒有爲了單元測試而存在的頂級目錄,相反,我們會將這些測試文件放在一個相對他們每一個需要測試的源代碼文件叫做_tests__
的目錄中。
例如,你測試setInnerHTML.js
的時候,你需要在相同目錄下建立一個__tests__
目錄來放它的單元測試文件,__tests__/setInnerHTML-test.js
5.分享代碼
儘管Haste
允許我們可以在代碼庫的任何地方導入模塊,但是我們爲了避免循環包容,按照約定,一個文件只能導入同目錄下或者是子目錄下的模塊。
例如一些文件在src/renderers/dom/stack/client
下面可能會導入在其他文件下的文件。
按照約定他是不能導入src/renderers/dom/stack/server
下的文件的,因爲它不是src/renderers/dom/stack/client
的子目錄。
如果我們要同時使用兩個模塊的功能的話,那麼可以在他們的最近公共祖先文件中建立一個shared
目錄來處理他們。
比如src/renderers/dom/stack/client
和src/renderers/dom/stack/server
有功能我要一起用,我就可以放在這個文件裏實現src/renderers/dom/shared
又比如src/renderers/dom/stack/client
和src/renderers/native
就可以放在src/renderers/shared
中。
6.警告和不變量
React
代碼庫使用了warning
模塊去警告。
var warning = require('warning');
warning(
2 + 2 === 4,
'Math is not working today.'
);
當第一個條件爲false
時,第二條信息就會展現。
有一個問題注意的事,這個warn
只是用警告非異常錯誤,因爲異常錯誤,比如說語法等等,console
會幫我們處理,我們不要去交叉他們的功能。
var warning = require('warning');
var didWarnAboutMath = false;
if (!didWarnAboutMath) {
warning(
2 + 2 === 4,
'Math is not working today.'
);
didWarnAboutMath = true;
}
當然一般警告只是在開發過程中,而實際的產品中這一部分一般都會被剔除掉,你可以通過改爲調用invariant
來代替。
var invariant = require('invariant');
invariant(
2 + 2 === 4,
'You shall not pass!'
);
你可以認爲這種方式是一種斷言。
我們要儘可能保持開發和最後的應用代碼基本一致,invariant
在產品中會自動將打印出錯誤的信息。
7.開發和產品
你可以使用__DEV__
僞全局變量來控制一段代碼塊。
在編譯器中會轉換爲process.env.NODE_ENV!=='production'
去區分產品還是開發。
通過if
判斷來確定是處於開發還是產品階段
if (__DEV__) {
// This code will only run in development.
}
8.JSDoc
JSDoc
是一個根據javascript
文件中註釋信息,生成JavaScript
應用程序或庫、模塊的API
文檔 的工具。
JSDoc
本質是代碼註釋,所以使用起來非常方便,但是它有一定的格式和規則,只有先了解這些,才能進行接下的工作那麼比如生產文檔,生成智能提示都可以通過工具來完成。
JSDoc
註釋一般應該放置在方法或函數聲明之前,它必須以/ **
開始,以便由JSDoc
解析器識別。其他任何以/*
,/***
或者超過JSDo
c解析器忽略。
/**
* Updates this component by updating the text content.
*
* @param {ReactText} nextText The next text content
* @param {ReactReconcileTransaction} transaction
* @internal
*/
receiveComponent: function(nextText, transaction) {
// ...
},
其中@
開頭的是一個特殊的註釋標籤,至於什麼意思建議大家去官方看看,這裏就不提太多,而Facebook
好像沒有使用這種方式,而是使用Flow
類型檢測工具來進行處理。
8.Flow
這裏提一下Facebook
的一個類型檢測的牛逼東西,Flow
–javascript
類型檢測,當你在你的許可頭中增加了@flow
這樣的註釋標籤後,這個文件就會被自動進行類型檢測。
咦,類型檢測到底是什麼鬼,這玩意都快自成一個體系,我也說不了多少,就簡簡單單的解釋一下。
Flow
是Facebook
公開的一個開源javascript
靜態類型檢測器,旨在發現javascript
程序中的類型錯誤,以此提高程序員開發程序的效率和代碼質量,非常快速也方便,可以很精確的判斷當前的函數調用或者其他的數據類型是否正確,提供官方地址:自己學去吧,涉及到類型註解啊,Flow類型系統的工作原理啊,怎麼配置安裝啊,等等等。https://flow.org/en/docs/
9.類和Mixin
的區分
React
最開始是用ES5
來寫的,後面自從Babel
出來後,就開始支持ES6
了,然而,現在大部分社區成員依舊用ES5
來寫,原因大家也懂,兼容性,可想而知,低版本的瀏覽器是有多不支持ES6
,只能在此一笑。
一般來說,你可能會看到如下的一些代碼
// Constructor
function ReactDOMComponent(element) {
this._currentElement = element;
}
// Methods
ReactDOMComponent.Mixin = {
mountComponent: function() {
// ...
}
};
// Put methods on the prototype
Object.assign(
ReactDOMComponent.prototype,
ReactDOMComponent.Mixin
);
module.exports = ReactDOMComponent;
上述的Mixin
和我們React
提到的Mixins
多繼承並沒有直接的聯繫,他只是一個種打包函數的方式,讓這些函數之後可以用在其他類上,即便是我們要儘量避免他,但是在某些地方使用這種模式確實是不錯的。
而寫成ES6
就是如下:
class ReactDOMComponent {
constructor(element) {
this._currentElement = element;
}
mountComponent() {
// ...
}
}
module.exports = ReactDOMComponent;
有時候我們會將非ES6
代碼轉換爲ES6
代碼,然而,這個並沒有什麼必要性,雖然官方推薦使用ES6
語法,但是一些方法在ES6
上並沒有得到很好的實現比如說這裏的Mixins
多繼承方式,在ES6
總沒有很好的實現,我們前面說的Mixins
多繼承也是在React.createClass
的ES5
風格代碼下使用的就即將出來的ES7
中或許有解決方法比如說修飾器。
10.動態注入
React
在某些模塊上使用動態注入,雖然他的目的非常明確,但是非常不幸的是,他阻礙了對代碼的理解,最開始的React
只是單單爲DOM
元素服務的,由於React Native
開始作爲React
項目的分支,才導致React
開發者不得不增加動態注入模塊以支持React Native
的使用。
如果你看過一些模塊,你會發現他們的動態注入方式如同下面:
// Dynamically injected
var textComponentClass = null;
// Relies on dynamically injected value
function createInstanceForText(text) {
return new textComponentClass(text);
}
var ReactHostComponent = {
createInstanceForText,
// 提供一個動態注入
injection: {
injectTextComponentClass: function(componentClass) {
textComponentClass = componentClass;
},
},
};
module.exports = ReactHostComponent;
在React DOM
中,ReactDefaultInjection
注入了一個DOM
實例
ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent);
在React Native
中,ReactNativeDefaultInjection
注入了他自己的一個實例
ReactHostComponent.injection.injectTextComponentClass(ReactNativeTextComponent);
以後這種機制可能會被取代掉。
11.多重包
React
是一個monorepo
模型(該模式特點自行百度),它的代碼庫包含多個分離的包以至於他們變化時可以相互協調,可以分開編譯打包測試。
npm
的元數據比如說package.json
被放在一個頂級文件夾packages中,但是這個文件夾中除了這玩意基本沒有什麼代碼。
比如packages/react/react.js
,真實接口其實在 src/isomorphic/React.js
。
雖然代碼被分離,但是npm
包和brower
包是不同的,所以要注意。
12.React
核心
React
的核心就是所有頂級API
,舉幾個例子
React.createElement()
React.createClass()
React.Component
React.Children
React.PropTypes
React
核心僅包含一些定義組件要用的API
,他不包括調解器算法或者其他爲了解決平臺跨瀏覽器的代碼。這個核心被React DOM
和React Native
組件所使用的。
核心代碼在src/isomorphic
中,如果大家要看請到github
中查看,在npm
中作爲一個react
包下載,而在瀏覽器環境下則是react.js
來處理,形成一個全局變量React
。
注意
如今很多核心代碼都被分離,已經不能算是核心代碼。
13.渲染器(Renderers
)
即便是React
接受了移動平臺React Native
,有一點不會變,那就是React
最開始是爲DOM
而生的,這一小部分將介紹一個React
內部的”渲染器”。
渲染器管理一個React
樹是怎麼轉換爲底層調用的。
渲染器源代碼在src/renders
中。
React DOM
渲染器渲染React
組件到DOM
中,它實現了一個ReactDOM
來處理,在npm
中我們會導入react-dom
,而瀏覽器端則用react-dom.js
來形成一個ReactDOM
全局操作接口。React Native
渲染器將React
組件渲染到移動視圖中,它一般是調用React Native
的react-native-renderer
來處理,這一部分在將來可能會被嵌入React Native
的代碼庫中,來滿足React
的更新。React Test
渲染器渲染React
組件到JSON
樹中,這個東西一般都已依賴於測試工具Jest
或者其他,在npm
包管理中,直接下載react-test-renderer
即可。
其他官方支持的渲染器就只有react-art
了,是一個圖形化渲染器。
14.解調器(Reconcilers
)
在之前說了渲染器的幾種,不然,即便是這幾種渲染器截然不同,但是他們都需要使用共享功能或者是會共享底層實現模塊邏輯。
尤其是,各個渲染器中的解調器算法應該基本一致,這樣纔可以讓聲明式渲染,自定義組件,狀態,生命週期,refs
實例,在跨平臺上顯示的效果一致。
爲了解決這個問題,不同的渲染器的底層代碼需要一致,功能需要一致,在React
稱這一部分功能爲調解器,當一個更新(setState()
)被激活時,我們的調解器就會調用組件的render()
去更新樹,或是裝載,卸載,等行爲。
在React
中的調解器暫時無法被單獨拿出來,因爲它暫時還沒有公共API
可以供上層應用直接調用,但是,渲染器(React DOM
和React Native
)處理得很好,可以非常合理的使用它。
15.堆棧調解器
堆棧調解器是現在所有React
產品的重要組成部分,核心中的核心,它在src/renderers/shared/stack/reconciler
中,同時被React DOM
和React Native
使用。
它是使用面對對象的方法實現的,作用是用來維護所有React
組件的內部實例所構造出的單獨的樹。這些內部實例包括用戶自定義的組件,也包括平臺元素即DOM
標籤,這些內部實例無法直接給用戶使用,他們都是透明的。
當一個組件裝載,更新,卸載時,堆棧調解器就會調用在這些實例上的一些方法來處理他們,這些方法:mountComponent(element)
, receiveComponent(nextElement)
, 和unmountComponent(element)
.
Host Components
(DOM
組件)
平臺特殊組件又名Host
後面基本就用這個名字吧,Host
,如同<div>
這一類型的組件。他們在處理時會運行專門爲他們準備的平臺特殊的代碼。例如,React DOM
會指示堆棧調解器去使用ReactDOMComponent
去處理裝載,更新,卸載DOM
組件。
先不管平臺問題,DOM
標籤會使用類似的方法來處理他們的孩子,爲了方便,堆棧調解器提供了一個叫做ReactMultiChild
的幫手來幫助在DOM
和移動平臺上渲染使用。
Composite Components
(組合組件)
用戶自定義的組件叫做組合組件,這種組件在所有的渲染器中都應該表現出相同的效果,這也是爲什麼堆棧調解器在ReactCompositeComponent
類中提供了一個render
的共享實現。
組合組件也實現了裝載,更新,卸載,但是不像host
組件,ReactCompositeComponent
怎麼表現或者渲染成什麼樣子都取決於用戶的代碼,這就是爲什麼他可以在用戶自定義的組件調用一些方法,如同render
和componentDidMount
等。
在更新過程中,ReactCompositeComponent
會檢查render
出來的數據是否跟前一個狀態的type
,key
有不同,如果相同則會拋棄這次孩子節點的更新,然後遞歸到子內部實例中,否則就會卸載舊的實例,裝載新的,這部分在之前的調解器或者是更新博客已經講到。
(這裏有一點需要區分的是,我們進行更新操作的目標永遠都是子節點DOM
,爲什麼呢,因爲我們的組件是不會加入到瀏覽器中的,加入瀏覽器的都是真實的DOM
節點)
Recursion
(遞歸)
在更新的時候,堆棧調解器會“向下探索”穿過組合組件,調用他們的render
方法,然後絕對是否是更新還是替換掉他們的單一的子孩子實例,然後它會通過host
組件去執行平臺特殊代碼,host
組件可能有多層孩子那麼就會遞歸去處理他們。
去理解堆棧調解器在一次流程中如何同步的處理組件樹是有必要的,然而個別樹分子可能會脫離調解器可以處理的範圍,而堆棧調解器又不會阻止,所在在CPU
資源被限制的條件下我們需要確保我們遞歸更新時的子最優或者說是儘可能不更新,防止更新帶來過多的資源消耗。
在下一篇博客中我將講述更多堆棧調解器的一些細節。
16.纖維調解器
這個纖維調解器的存在是爲了解決在堆棧調解器中長期存在的一些問題。
這是一個完全重寫的調解器,不同的思路,不同的方法,如今這個調解器牛逼人士們正在積極的研究當中,相信不久就能夠出來了,期待啊。
它的幾個主要的目的:
在大型項目中可以分離出可中斷的工作
在項目開發中能更好的優化序列,重定位基準,功能可重用性
可以在父親和孩子組件之間可以來回傳遞。
用
render()
不需要限制一定只能有一個父元素更準確的識別出錯誤
你如果想更多的瞭解纖維調解器,github
地址:src/renderers/shared/fiber
17.事件系統
React
實現了合成事件,這部分屬於共享代碼,React DOM
和React Native
中都有。
src/renderers/shared/shared/event
.
18.add-ons
這一部分額外工具在src/addons
目錄,有興趣的可以去看看源代碼
下一篇將講
React
中調解器的實現細節了