淺談React Native與實(shí)現(xiàn)機(jī)制

1. React Native簡(jiǎn)單介紹

目前App開發(fā)的主流方式有三種: Native開發(fā),Hybird開發(fā)以及Web開發(fā)

原生Native開發(fā)

主要采用Object-C/Swift方式進(jìn)行原生開發(fā)。運(yùn)行效率高,流暢,用戶體驗(yàn)好,可以做各種復(fù)雜的動(dòng)畫效果。平臺(tái)獨(dú)立性,代碼無法在其他平臺(tái)上運(yùn)行,無法做到跨平臺(tái)。更新審核周期比較長(zhǎng),不利于App問題的快速修復(fù)

Hybird開發(fā)

以原生開發(fā)為主。
更新頻繁,活動(dòng)頁(yè)面,運(yùn)營(yíng)頁(yè)面等采用H5方式接入。定義好原生功能與H5之間的協(xié)議,攔截特定的URL Schema進(jìn)行原生功能的調(diào)用,App調(diào)用H5提供的js方法,給H5傳值和通知H5

Web開發(fā)

是Web App,以Web為主,通過js或者插件方式調(diào)用原生功能,如撥打電話,位置服務(wù)等。
一套Web代碼可以分別在各個(gè)平臺(tái)上運(yùn)行。受限制與UIWebView,app的性能和體驗(yàn)都無法與純?cè)鷄pp相提并論。比較有代表性的:采用cordova和ionic進(jìn)行web app開發(fā),通過開發(fā)原生插件功能供Web端調(diào)用

React Native的出現(xiàn)

不同的開發(fā)方式都在解決如下的幾個(gè)問題

  • 使得APP的體驗(yàn)效果和原生應(yīng)用一樣好
  • 跨平臺(tái),提高項(xiàng)目代碼的重用性
  • 應(yīng)對(duì)廣告或者活動(dòng)更新,能夠進(jìn)行熱替換而不用進(jìn)行APP新發(fā)布

因此Facebook在2015年發(fā)布了React Native框架,旨在幫助前端程序員解決如上的棘手問題,在發(fā)布當(dāng)初,相比于其他Hybird框架,React Native有如下的特點(diǎn)

  • 基于組件開發(fā),提供代碼的復(fù)用率。
  • 各個(gè)平臺(tái)功能代碼可以進(jìn)行復(fù)用,官方數(shù)據(jù)表明:iOS和android功能代碼可以達(dá)到90%以上的復(fù)用。
  • 不用Webview,徹底擺脫了Webview的限制:交互和性能問題。
  • 相對(duì)其它Hybrid 方案,React Native性能更好,用戶體驗(yàn)更接近原生。
  • 減少編譯時(shí)間,提高開發(fā)效率。
  • 可以采用熱更新方式進(jìn)行app功能升級(jí)和問題修復(fù),提高app的迭代率和開發(fā)效率
React Native實(shí)例
  • 在JS語言中嵌入了HTML和CSS的元素,這種被擴(kuò)展了的JavaScript語言稱為jsx
  • React Native框架中,JavaScript內(nèi)存中維護(hù)了一個(gè)Virtual DOM,JSX內(nèi)容在Virtual DOM中被轉(zhuǎn)化翻譯成真實(shí)的DOM樹,Virtual DOM與真實(shí)顯示的DOM保持一一對(duì)應(yīng)的關(guān)系
  • 當(dāng)界面發(fā)生變化時(shí),得益于高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動(dòng) DOM,避免了重新繪制 DOM
JSX實(shí)例
var MyComponent = React.createClass({
  handleClick: function() {
    this.refs.myTextInput.focus();
  },
  render: function() {
    return (
      <div>
        <input type="text" ref="myTextInput" />
        <input type="button" value="Focus the text input" onClick={this.handleClick} />
      </div>
    );
  }
});

ReactDOM.render(
  <MyComponent />,
  document.getElementById('example')
);

2. 動(dòng)態(tài)配置

在當(dāng)下移動(dòng)端App越來越人性化的趨勢(shì)下,App的更新迭代速度很快,但是限制于App Store和安卓市場(chǎng)的應(yīng)用版本更新限制,如果每次界面上有部分需要更新,例如廣告更換,頁(yè)面布局調(diào)整等,都需要通過發(fā)布一個(gè)新的版本來實(shí)現(xiàn),著實(shí)對(duì)應(yīng)用商和用戶來說都是不合理的,因此如果有一種方式可以動(dòng)態(tài)的配置移動(dòng)端界面便是極好的。

很多時(shí)候,我們都是利用 JSON 文件實(shí)現(xiàn)動(dòng)態(tài)配置的效果,它的核心流程如下

JSON實(shí)現(xiàn)動(dòng)態(tài)配置
  1. 通過 HTTP 請(qǐng)求獲取 JSON 格式的配置文件。
  2. 配置文件中標(biāo)記了每一個(gè)元素的屬性,比如位置,顏色,圖片 URL 等。
  3. 解析完 JSON 后,我們調(diào)用 Objective-C 的代碼,完成 UI 控件的渲染。

通過這種方法,我們實(shí)現(xiàn)了在后臺(tái)配置 app 的展示樣式。從本質(zhì)上來說,移動(dòng)端和服務(wù)端約定了一套協(xié)議,但是協(xié)議內(nèi)容嚴(yán)重依賴于應(yīng)用內(nèi)要展示的內(nèi)容,不利于拓展。也就是說,如果業(yè)務(wù)要求頻繁的增加或修改頁(yè)面,這套協(xié)議很難應(yīng)付。

然而這種通過JSON通信,配置一些可選項(xiàng)的方式在很多情況下都不能夠滿足現(xiàn)在快速迭代開發(fā)的App,如果想要改變一些業(yè)務(wù)邏輯或者進(jìn)行一些復(fù)雜度比較高的修改操作,則客戶端只讀取JSON配置項(xiàng)是做不到的,無法調(diào)用處理業(yè)務(wù)邏輯的方法等,不具備調(diào)試功能。

各種移動(dòng)平臺(tái)支持JavaScript

然而,基于現(xiàn)在移動(dòng)設(shè)備都支持JavaScript代碼的執(zhí)行這一條件(例如iOS上內(nèi)置了JavaScript Core來執(zhí)行JavaScript代碼),React Native的推出發(fā)揮了這一優(yōu)點(diǎn),通過JavaScript代碼,不僅僅只是傳遞簡(jiǎn)單的配置信息,更可以進(jìn)行業(yè)務(wù)邏輯的處理。

Learn once, write everywhere

和其他Hybird框架所宣傳的"Write once, run everywhere"不同,React Native其實(shí)不能真正意義上稱為"跨平臺(tái)"框架,因?yàn)樗谋举|(zhì)是使用了各個(gè)移動(dòng)平臺(tái)都支持JavaScript語言,React Native幫我們做好了將JavaScript代碼轉(zhuǎn)化成Object-C或者Java語言,并且?guī)臀覀兲幚砗昧烁鞣N回調(diào)問題,因此表面上我們只需要編寫JavaScript語言,即可在不同的平臺(tái)上展現(xiàn)應(yīng)用,這也是React Native的開發(fā)初衷: 分別開發(fā)安卓和iOS而不用寫一行原生代碼

3. 通信機(jī)制

iOS -- JavaScript / Objective-C

我們雖然使用的是React Native框架,但還是需要依賴UIKit等框架,從而調(diào)用Objective-C代碼以在iOS平臺(tái)上執(zhí)行,JavaScript其實(shí)只是為我們提供了編寫業(yè)務(wù)邏輯和前端界面的輔助工具,React Native在iOS上能夠運(yùn)行的實(shí)質(zhì)是利用JS代碼調(diào)用OC代碼執(zhí)行,我們需要關(guān)注的重點(diǎn)就是JS與OC之間的通信機(jī)制,包括JS是如何去調(diào)用OC代碼,又如何實(shí)現(xiàn)回調(diào)功能,這是React Native的核心功能之一。

我們都知道,JS作為一種腳本語言,是不需要像C語言那樣被編譯鏈接然后執(zhí)行,在執(zhí)行腳本語言時(shí),會(huì)在運(yùn)行時(shí)動(dòng)態(tài)地進(jìn)行詞法和語法的分析,然后生成一課抽象語法樹和對(duì)應(yīng)的字節(jié)碼,由JS解釋器等將字節(jié)碼轉(zhuǎn)化成對(duì)應(yīng)的機(jī)器碼,而整個(gè)流程都是由JS引擎來加以完成。

JavaScript Core

在iOS平臺(tái)下,React Native利用了iOS提供的JavaScript Core作為JS解析器,然而RN并沒有完全使用JS Core中提供的JS-OC互調(diào)的特性,而是自己實(shí)現(xiàn)了一套通用的方案,以便兼容不同的版本

OC調(diào)用JS

OC向JS傳信息有現(xiàn)成接口,stringByEvaluatingJavaScriptFromString方法可以直接在當(dāng)前context上執(zhí)行一段JS腳本,并且可以獲取執(zhí)行后的返回值,這個(gè)返回值就相當(dāng)于JS向OC傳遞信息。

JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];

在上述例子中,JSContext 代表了當(dāng)前JS的執(zhí)行環(huán)境,evaluateScript 方法則會(huì)去執(zhí)行后面跟著的js腳本內(nèi)容,返回值會(huì)存放在 JSValue 中,從而完成OC調(diào)用JS并獲取JS返回信息。

JS調(diào)用OC

React Native基于上述OC調(diào)用JS的方法,經(jīng)過一些封裝在OC里面定義了一個(gè)模塊方法,JS可以直接調(diào)用這個(gè)模塊方法并且可以注冊(cè)回調(diào)函數(shù)。

//Objective-C
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
     RCT_EXPORT();
     NSString *ret = @"ret"
     responseSender(ret);
}
@end
//JavaScript
RCTSQLManager.query("SELECT * FROM table", function(result) {
     //result == "ret";
});

如上圖所示,在OC內(nèi)部定義了一個(gè)模塊RCTSQLManager,并且在模塊內(nèi)部定義了方法 -query: successCallback;我們?cè)贘S中可以直接調(diào)用RCTSQLManager的query方法并且注冊(cè)回調(diào)函數(shù)。

模塊配置表
  1. 取出所有可被調(diào)用的模塊,每個(gè)可被調(diào)用模塊類都實(shí)現(xiàn)了RCTBridgeModule接口,可以通過runtime接口objc_getClassListobjc_copyClassList取出項(xiàng)目里所有類,然后逐個(gè)判斷是否實(shí)現(xiàn)了RCTBridgeModule接口,就可以找到所有模塊類。

  2. 取出模塊中所有可被調(diào)用的方法,模塊方法里有句代碼:RCT_EXPORT(),模塊里的方法加上這個(gè)宏就可以實(shí)現(xiàn)暴露給JS,無需其他規(guī)則,這個(gè)宏的作用是用編譯屬性__attribute__給二進(jìn)制文件新建一個(gè)section,屬于__DATA數(shù)據(jù)段,名字為RCTExport,并在這個(gè)段里加入當(dāng)前方法名。編譯器在編譯時(shí)會(huì)找到__attribute__進(jìn)行處理,為生成的可執(zhí)行文件加入相應(yīng)的內(nèi)容。

#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
  1. 在讀取完所有可被調(diào)用模塊和可被調(diào)用方法后,OC告訴JS有哪些模塊,哪些方法是可以被JS調(diào)用的,在這里的實(shí)現(xiàn)機(jī)制是OC生成一份模塊配置表然后OC端和JS端分別持有這一份配置表,表的內(nèi)容大致如下,可以看到每個(gè)模塊都有對(duì)應(yīng)的編號(hào),每個(gè)方法也有對(duì)應(yīng)的編號(hào),在JS調(diào)用OC時(shí),通過傳遞對(duì)應(yīng)的ModuleID和MethodID即可匹配OC模塊及方法。
{
    "remoteModuleConfig": {
        "RCTSQLManager": {
            "methods": {
                "query": {
                    "type": "remote",
                    "methodID": 0
                }
            },
            "moduleID": 4
        },
        ...
     },
}
React Native初始化分析

每個(gè)應(yīng)用有一個(gè)唯一的 rootWindow,每一個(gè)UIWindow有一個(gè)唯一的rootView,在React Native 中,對(duì)應(yīng)的就是RCTRootView,它持有一個(gè)RCTBridge,RCTBridge的職能是通訊橋,負(fù)責(zé)各個(gè)模塊之間和js之間的通訊, RCTBatchedBridge繼承RCTBridge,它有一個(gè)唯一的但是可變的currentBridge,實(shí)際上RCTBridge是唯一的, RCTBatchedBridge是唯一的,通訊時(shí),實(shí)際上RCTBatchedBridge承擔(dān)一個(gè)適配的職責(zé)。

因此,實(shí)際上在創(chuàng)建一個(gè)RootView之前,React Native都會(huì)預(yù)先創(chuàng)建好一個(gè)RCTBridge,而RCTBridgesetUp方法主要是為了初始化BatchedBridgeBatchedBridge主要是用來批量讀取JavaScript對(duì)Objective-C的調(diào)用,BatchedBridge內(nèi)部還依賴一個(gè)JSCExecutor,用于執(zhí)行JS代碼,下面我們簡(jiǎn)單地了解一下BatchedBridge初始化過程中都做了哪些工作。

1. 讀取 JavaScript 源碼

這個(gè)過程將應(yīng)用的js代碼加載到內(nèi)存,供接下來在OC中調(diào)用執(zhí)行JS代碼

2. 初始化模塊信息

這一步主要是發(fā)現(xiàn)所有需要暴露給JavaScript的模塊及模塊中需要暴露的方法,每一個(gè)需要暴露的模塊都會(huì)被加上 RCT_EXPORT_MODULE的宏,宏的內(nèi)容如下:

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

這個(gè)類在執(zhí)行l(wèi)oad方法時(shí)會(huì)調(diào)用RCTRegisterModule方法,將自身注冊(cè)到RCTModuleClasses

void RCTRegisterModule(Class moduleClass)
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    RCTModuleClasses = [NSMutableArray new];
  });

  [RCTModuleClasses addObject:moduleClass];
}

因此我們可以從RCTModuleClasses中獲取出所有模塊信息,每一條模塊信息都被存儲(chǔ)與RCTModuleData對(duì)象中

for (Class moduleClass in RCTGetModuleClasses()) {
    RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass                                                                      bridge:self];
    [moduleClassesByID addObject:moduleClass];
    [moduleDataByID addObject:moduleData];
}
3. 初始化 JavaScript 代碼的執(zhí)行器,即 RCTJSCExecutor 對(duì)象

在這一步操作中,通過addSynchronousHookWithName這一方法向JavaScript的添加了若干的Block對(duì)象作為全局變量,以供第5步過程中在執(zhí)行JavaScript源碼時(shí)處理這些Block對(duì)象

4. 生成模塊列表并寫入 JavaScript 端

這一步操作是將OC端生成的模塊列表信息注入到JavaScript端中,以便雙方都持有一份模塊列表信息。

- (NSString *)moduleConfig{
    NSMutableArray<NSArray *> *config = [NSMutableArray new];
    for (RCTModuleData *moduleData in _moduleDataByID) {
      [config addObject:@[moduleData.name]];
    }
}

可以看到,Objective-C將config信息存儲(chǔ)到了JavaScript的全局變量中,名稱為__fbBatchedBridgeConfig

5. 執(zhí)行 JavaScript 源碼

在所有的初始化都完成后,只需要運(yùn)行js代碼即可,運(yùn)行過程中也會(huì)執(zhí)行第3步過程添加進(jìn)全局變量的Block對(duì)象

方法調(diào)用流程
  1. JS調(diào)用模塊方法。

  2. 在JS端有一個(gè)JS Bridge專門負(fù)責(zé)處理JS與OC交互部分,同理在OC端也有一個(gè)OC Bridge,JS Bridge將調(diào)用的模塊方法記錄并轉(zhuǎn)化成相應(yīng)的ModuleName,MethodName和args。

  3. 然后在MessageQueue中將調(diào)用模塊方法時(shí)的回調(diào)函數(shù)注冊(cè)一個(gè)CallBack ID,將ID和回調(diào)函數(shù)存儲(chǔ)在一個(gè)成員變量的列表中,并將第2步中的ModuleName和MethodName根據(jù)模塊配置表信息轉(zhuǎn)成對(duì)應(yīng)的ID。

  4. JS將moduleID,methodID和args以及CallBackID傳遞給OC Bridge,這個(gè)過程實(shí)質(zhì)上是基于事件處理的,因?yàn)樵谝苿?dòng)平臺(tái)上如果有代碼的執(zhí)行必定是某個(gè)事件觸發(fā)的,比如滑動(dòng)屏幕等等,事件觸發(fā)后OC主動(dòng)調(diào)用JS代碼,JS處理業(yè)務(wù)邏輯過程并將需要調(diào)用OC的部分存儲(chǔ)到MessageQueue中,再去通知OC執(zhí)行。

  5. OC接收到消息,通過模塊配置表拿到對(duì)應(yīng)的模塊和方法,在OC Bridge端,對(duì)每一個(gè)可被調(diào)用的模塊方法都會(huì)有一個(gè)RCTModuleMethod對(duì)象與之對(duì)應(yīng)。

  6. RCTModuleMethod對(duì)傳進(jìn)來的參數(shù)進(jìn)行處理,包括類型轉(zhuǎn)化以及創(chuàng)建一個(gè)Block對(duì)象以供回調(diào),會(huì)將JS端傳過來的CallBackID以及回調(diào)的值存儲(chǔ)進(jìn)Block對(duì)象中

  7. 執(zhí)行OC端代碼

  8. 執(zhí)行第6步中生成的Block方法

  9. block里帶著CallbackID和block傳過來的參數(shù)去調(diào)JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

  10. MessageQueue根據(jù)CallBackId找到對(duì)應(yīng)的回調(diào)函數(shù)

  11. 根據(jù)OC傳來的回調(diào)值,執(zhí)行回調(diào)函數(shù)

整個(gè)流程就是這樣,簡(jiǎn)單概括下,差不多就是:JS函數(shù)調(diào)用轉(zhuǎn)ModuleID/MethodID -> callback轉(zhuǎn)CallbackID -> OC根據(jù)ID拿到方法 -> 處理參數(shù) -> 調(diào)用OC方法 -> 回調(diào)CallbackID -> JS通過CallbackID拿到callback執(zhí)行

4. React Native優(yōu)缺點(diǎn)

優(yōu)點(diǎn)
  • 能夠利用 JavaScript 動(dòng)態(tài)更新的特性,快速迭代。
  • 相比于原生平臺(tái),開發(fā)速度更快,相比于 Hybrid 框架,性能更好。
缺點(diǎn)
  • 不能實(shí)現(xiàn)真正意義上的跨平臺(tái),開發(fā)者仍然需要為iOS和Android提供兩套實(shí)現(xiàn)機(jī)制
  • 不能直接取代Native Code開發(fā),很大程度上還加重了開發(fā)者的學(xué)習(xí)成本
  • 語言互轉(zhuǎn)存在著固定的時(shí)間和空間開銷

參考資料

  1. React Native從入門到原理
  2. React Native通信原理
  3. React Native通信機(jī)制詳解
  4. React Native開發(fā)簡(jiǎn)介
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容