React學習之相關代碼庫(三十六)

本章將講述React代碼庫的組織,約定,和它的實現方式。

如果你想更加關注React,或者說作爲開發貢獻者,對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 setInnerHTMLgithub地址,在新文件中,你只要對應着修改就可以了。

  • 在導入時別使用相對路徑,不寫require('./setInnerHTML'),而寫require('setInnerHTML')

當我們使用npm來處理React時,一個腳本會複製所有的模塊到一個叫做lib文件夾中,這種方式就是用require加絕對或者相對地址來處理,在Nodejs,browerify,webpack和其它一些工具都是這樣來處理React模塊的,但是這和Haste並沒有什麼關係。

2.外部依賴關係

React一般都沒有什麼外部依賴,但是比如fbjsRelay等等,雖然不是React的公共API,但是都是Facebook內部分離出來的,不能算是一種外部依賴。

3.頂級文件夾

通過查看React的庫,我們可以看到如下的文件結構:

  • srcReact的源代碼文件夾,如果你要修改相關代碼,src你可能需要花大部分時間去看看,學習學習

  • docsReact文檔網頁夾,如果你改變了API的話,就有必要更新相關文檔文件。

  • examples包含React demo的一部分事例

  • packages包含一些在React代碼庫中的代碼分佈結構(比如package.json)。

  • buildReact 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/clientsrc/renderers/dom/stack/server有功能我要一起用,我就可以放在這個文件裏實現src/renderers/dom/shared

又比如src/renderers/dom/stack/clientsrc/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解析器識別。其他任何以/*/***或者超過3 個星號的註釋,都將被JSDoc解析器忽略。

/**
  * 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的一個類型檢測的牛逼東西,Flowjavascript類型檢測,當你在你的許可頭中增加了@flow這樣的註釋標籤後,這個文件就會被自動進行類型檢測。

咦,類型檢測到底是什麼鬼,這玩意都快自成一個體系,我也說不了多少,就簡簡單單的解釋一下。

FlowFacebook公開的一個開源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.createClassES5風格代碼下使用的就即將出來的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 DOMReact 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 Nativereact-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 DOMReact Native)處理得很好,可以非常合理的使用它。

15.堆棧調解器

堆棧調解器是現在所有React產品的重要組成部分,核心中的核心,它在src/renderers/shared/stack/reconciler中,同時被React DOMReact 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 怎麼表現或者渲染成什麼樣子都取決於用戶的代碼,這就是爲什麼他可以在用戶自定義的組件調用一些方法,如同rendercomponentDidMount等。

在更新過程中,ReactCompositeComponent會檢查render出來的數據是否跟前一個狀態的typekey有不同,如果相同則會拋棄這次孩子節點的更新,然後遞歸到子內部實例中,否則就會卸載舊的實例,裝載新的,這部分在之前的調解器或者是更新博客已經講到。

(這裏有一點需要區分的是,我們進行更新操作的目標永遠都是子節點DOM,爲什麼呢,因爲我們的組件是不會加入到瀏覽器中的,加入瀏覽器的都是真實的DOM節點)

Recursion(遞歸)

在更新的時候,堆棧調解器會“向下探索”穿過組合組件,調用他們的render方法,然後絕對是否是更新還是替換掉他們的單一的子孩子實例,然後它會通過host組件去執行平臺特殊代碼,host組件可能有多層孩子那麼就會遞歸去處理他們。

去理解堆棧調解器在一次流程中如何同步的處理組件樹是有必要的,然而個別樹分子可能會脫離調解器可以處理的範圍,而堆棧調解器又不會阻止,所在在CPU資源被限制的條件下我們需要確保我們遞歸更新時的子最優或者說是儘可能不更新,防止更新帶來過多的資源消耗。

在下一篇博客中我將講述更多堆棧調解器的一些細節。

16.纖維調解器

這個纖維調解器的存在是爲了解決在堆棧調解器中長期存在的一些問題。
這是一個完全重寫的調解器,不同的思路,不同的方法,如今這個調解器牛逼人士們正在積極的研究當中,相信不久就能夠出來了,期待啊。

它的幾個主要的目的:

  1. 在大型項目中可以分離出可中斷的工作

  2. 在項目開發中能更好的優化序列,重定位基準,功能可重用性

  3. 可以在父親和孩子組件之間可以來回傳遞。

  4. render()不需要限制一定只能有一個父元素

  5. 更準確的識別出錯誤

你如果想更多的瞭解纖維調解器,github地址:src/renderers/shared/fiber

17.事件系統

React實現了合成事件,這部分屬於共享代碼,React DOMReact Native中都有。
src/renderers/shared/shared/event.

18.add-ons

這一部分額外工具在src/addons目錄,有興趣的可以去看看源代碼

下一篇將講React中調解器的實現細節了

發佈了447 篇原創文章 · 獲贊 471 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章