React
我想動態(tài)修改一個(gè)按鈕的文字,需要這樣寫:
<button type="button" id="button" onclick="onClick()">old button</button>
// 在 JavaScript 中操作 DOM:
<script>
function onClick() {
document.getElementById('button').innerHTML='new button';
}
</script>
可以看到,在 HTML 和 JavaScript 代碼中,id 和 onclick 事件觸發(fā)的函數(shù)必須完全對應(yīng),否則就無法正確的響應(yīng)事件。如果想知道一個(gè) HTML 標(biāo)簽會如何被響應(yīng),我們還得跑去 JavaScript 代碼中查找,這種原始的配置方式很不方便。
于是FaceBook 推出了 React 框架,這個(gè)問題得到了大幅度改善。我們可以把一組相關(guān)的 HTML 標(biāo)簽,也就是 app 內(nèi)的 UI 控件,封裝進(jìn)一個(gè)組件(Component)中。阮一峰的 React 教程中摘錄的一段代碼:
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>
);
}
});
如果你想問:“為什么 JavaScript 代碼里面出現(xiàn)了 HTML 的語法”,那么恭喜你已經(jīng)初步體會到 React 的奧妙了。這種語法被稱為 JSX,它是一種 JavaScript 語法拓展。JSX 允許我們寫 HTML 標(biāo)簽或 React 標(biāo)簽,它們終將被轉(zhuǎn)換成原生的 JavaScript 并創(chuàng)建 DOM。
在 React 框架中,除了可以用 JavaScript 寫 HTML 以外,我們甚至可以寫 CSS,這在后面的例子中可以看到。
React 是一套可以用 簡潔 的語法 高效 繪制 DOM 的框架:
簡潔因?yàn)槲覀兛梢詴簳r(shí)放下 HTML 和 CSS,只關(guān)心如何用 JavaScript 構(gòu)造頁面
高效是因?yàn)?React 獨(dú)創(chuàng)了 Virtual DOM 機(jī)制。Virtual DOM 是一個(gè)存在于內(nèi)存中的 JavaScript 對象,
它與 DOM 是一一對應(yīng)的關(guān)系,也就是說只要有 Virtual DOM,我們就能渲染出 DOM。
當(dāng)界面發(fā)生變化時(shí),得益于高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪制 DOM。
React 并不是前端開發(fā)的全部,它專注于 UI 部分,對應(yīng)到 MVC 結(jié)構(gòu)中就是 View 層。要想實(shí)現(xiàn)完整的 MVC 架構(gòu),還需要 Model 和 Controller 的結(jié)構(gòu)。在前端開發(fā)時(shí),我們可以采用 Flux 和 Redux 架構(gòu),它們并非框架(Library),而是和 MVC 一樣都是一種架構(gòu)設(shè)計(jì)(Architecture)。
如果不從事前端開發(fā),就不用深入的掌握 Flux 和 Redux 架構(gòu),但理解這一套體系結(jié)構(gòu)對于后面理解 React Native 非常重要。
React Native
移動端通過 JSON 文件傳遞信息,只能傳遞配置信息,無法表達(dá)邏輯。
而 React 在前端取得突破性成功以后,JavaScript 布道者們開始試圖一統(tǒng)三端。他們利用了移動平臺能夠運(yùn)行 JavaScript 代碼的能力,并且發(fā)揮了 JavaScript 不僅僅可以傳遞配置信息,還可以表達(dá)邏輯信息的優(yōu)點(diǎn)。
于是一個(gè)基于 JavaScript,具備動態(tài)配置能力,面向前端開發(fā)者的移動端開發(fā)框架,React Native,誕生了!
這是一個(gè)面向前端開發(fā)者的框架。它的宗旨是讓前端開發(fā)者像用 React 寫網(wǎng)頁那樣,用 React Native 寫移動端應(yīng)用。
原理概述
-
React Native 與 Hybrid 完全沒有關(guān)系
- 即使使用了 React Native,我們依然需要 UIKit 等框架,調(diào)用的是 Objective-C 代碼。React Native只不過是以 JavaScript 的形式告訴 Objective-C 該執(zhí)行什么代碼。
-
React Native 能夠運(yùn)行起來,全靠 Objective-C 和 JavaScript 的交互。
我們知道 C 系列的語言,經(jīng)過編譯,鏈接等操作后,會得到一個(gè)二進(jìn)制格式的可執(zhí)行文,所謂的運(yùn)行程序,其實(shí)是運(yùn)行這個(gè)二進(jìn)制程序。
而 JavaScript 是一種腳本語言,它不會經(jīng)過編譯、鏈接等操作,而是在運(yùn)行時(shí)才動態(tài)的進(jìn)行詞法、語法分析,生成抽象語法樹(AST)和字節(jié)碼,然后由解釋器負(fù)責(zé)執(zhí)行或者使用 JIT 將字節(jié)碼轉(zhuǎn)化為機(jī)器碼再執(zhí)行。整個(gè)流程由 JavaScript 引擎負(fù)責(zé)完成。
JavaScript 是一種單線程的語言;
JavaScript不具備自運(yùn)行的能力,因此總是被動調(diào)用。
“JavaScript 線程” 實(shí)際上表示的是 Objective-C 創(chuàng)建了一個(gè)單獨(dú)的線程,這個(gè)線程只用于執(zhí)行 JavaScript 代碼,而且 JavaScript 代碼只會在這個(gè)線程中執(zhí)行。
-
蘋果提供了一個(gè)叫做 JavaScript Core 的框架,這是一個(gè) JavaScript 引擎:
- JSContext 指的是 JavaScript 代碼的運(yùn)行環(huán)境,通過方法
evaluateScript即可執(zhí)行 JavaScript 代碼獲取返回結(jié)果
- JSContext 指的是 JavaScript 代碼的運(yùn)行環(huán)境,通過方法
// Objective-C 如何調(diào)用 JavaScript :
JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
Objective-C 與 JavaScript 交互
Objective-C 和 JavaScript 的交互總是由前者發(fā)起
由于 JavaScript Core 是一個(gè)面向 Objective-C 的框架,在 Objective-C 這一端,我們對 JavaScript 上下文知根知底,可以很容易的獲取到對象,方法等各種信息,當(dāng)然也包括調(diào)用 JavaScript 函數(shù)。
真正復(fù)雜的問題在于,JavaScript 不知道 Objective-C 有哪些方法可以調(diào)用。
React Native 解決這個(gè)問題的方案是在 Objective-C 和 JavaScript 兩端都保存了一份配置表,里面標(biāo)記了所有 Objective-C 暴露給 JavaScript 的 模塊和方法。
無論是哪一方調(diào)用另一方的方法,實(shí)際上傳遞的數(shù)據(jù)只有 ModuleId、MethodId 和 Arguments 這三個(gè)元素,它們分別表示 類、方法和方法參數(shù)
JavaScript 解析出將調(diào)用方法的三個(gè)元素后放入到 MessageQueue 中,等待 Objective-C 拿走
當(dāng) Objective-C 接收到這三個(gè)元素后,就可以通過 runtime 唯一確定要調(diào)用的是哪個(gè)Objective-C函數(shù),然后調(diào)用這個(gè)函數(shù)
上述解決方案只是一個(gè)抽象概念,可能與實(shí)際的解決方案有微小差異,比如實(shí)際上 Objective-C 這一端,并沒有直接保存這個(gè)模塊配置表。具體實(shí)現(xiàn)將在下一節(jié)中隨著源碼一起分析。
React Native Objective-C端源碼分析
配置表的形成 (Objective-C 調(diào)用 JavaScript)
每個(gè)項(xiàng)目都有一個(gè)入口,然后進(jìn)行初始化操作,React Native 也不例外。一個(gè)不含 Objective-C 代碼的項(xiàng)目留給我們的唯一線索就是位于 AppDelegate 文件中的代碼:
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"PropertyFinder" initialProperties:nil launchOptions:launchOptions];
用戶能看到的一切內(nèi)容都來源于這個(gè) rootView,所有的初始化工作也都在這個(gè)方法內(nèi)完成。
在這個(gè)方法內(nèi)部,在創(chuàng)建 rootView 之前,React Native 實(shí)際上先創(chuàng)建了一個(gè) BatchedBridge 對象。它是 Objective-C 與 JavaScript 交互的橋梁,后續(xù)的方法交互完全依賴于它,而整個(gè)初始化過程的最終目的其實(shí)也就是創(chuàng)建這個(gè)橋梁對象。
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任務(wù)則是創(chuàng)建 BatchedBridge對象。
BatchedBridge對象的作用是批量讀取 JavaScript 對 Objective-C 的方法調(diào)用,同時(shí)它內(nèi)部持有一個(gè) RCTJSCExecutor對象 對象,用來執(zhí)行 JavaScript 代碼。
創(chuàng)建 BatchedBridge對象 的關(guān)鍵是 start 方法,start方法又分為五個(gè)步驟:
1.讀取JavaScript源碼
2.初始化模塊信息
3.初始化 JavaScript 代碼的執(zhí)行器(即 RCTJSCExecutor對象)
4.生成模塊列表并寫入 JavaScript 端
5.執(zhí)行 JavaScript 源碼
方法調(diào)用
如前文所述,在 React Native 中,Objective-C 和 JavaScript 的交互都是通過傳遞 ModuleId、MethodId 和 Arguments 進(jìn)行的。以下是分情況討論:
調(diào)用 JavaScript 方法
調(diào)用 JavaScript 代碼的核心代碼如下:
// 這個(gè)函數(shù)是我們要調(diào)用 JavaScript 的中轉(zhuǎn)函數(shù)。也就是說它的作用其實(shí)是處理參數(shù),而非真正要調(diào)用的 JavaScript 函數(shù)。
// 這個(gè)中轉(zhuǎn)函數(shù)接收到的參數(shù)包含了 ModuleId、MethodId 和 Arguments,然后由中轉(zhuǎn)函數(shù)查找自己的模塊配置表,找到真正要調(diào)用的 JavaScript 函數(shù)
- (void)_executeJSCall:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete {
[self executeBlockOnJavaScriptQueue:^{
// 獲取 contextJSRef、methodJSRef、moduleJSRef
resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef);
objcValue = /*resultJSRef 轉(zhuǎn)換成 Objective-C 類型*/
onComplete(objcValue, nil);
}];
}
在實(shí)際使用的時(shí)候,我們可以這樣發(fā)起對 JavaScript 的調(diào)用:
// Name 和 Body 參數(shù)分別表示要調(diào)用的 JavaScript 的函數(shù)名和參數(shù)
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": @"nmae"}];
調(diào)用 Objective-C方法
JavaScript 解析出方法的 ModuleId、MethodId 和 Arguments 并放入到 MessageQueue 中,等待 Objective-C 拿走 (或者超時(shí)后主動發(fā)送給 Objective-C)。
-
Objective-C通過查找模塊配置表找出要調(diào)用的方法,并通過 runtime 動態(tài)的調(diào)用
Objective-C 負(fù)責(zé)處理調(diào)用的方法是
handleBuffer,它的參數(shù)是一個(gè)含有四個(gè)元素的數(shù)組,每個(gè)元素也都是一個(gè)數(shù)組,分別存放了 ModuleId、MethodId、Params,第四個(gè)元素目測用處不大。函數(shù)內(nèi)部在每一次方法調(diào)用中調(diào)用
_handleRequestNumber:moduleID:methodID:params方法。通過查找模塊配置表找出要調(diào)用的方法,并通過 runtime 動態(tài)的調(diào)用:
[method invokeWithBridge:self module:moduleData.instance arguments:params];在這個(gè)方法中,有一個(gè)很關(guān)鍵的方法:
processMethodSignature,它會根據(jù) JavaScript 的 CallbackId 創(chuàng)建一個(gè) Block,并且在調(diào)用完函數(shù)后執(zhí)行這個(gè) Block。
JavaScript閉包的回調(diào)
既然說到函數(shù)互調(diào),那么就不得不提到回調(diào)了。對于 Objective-C 來說,執(zhí)行完 JavaScript 代碼再執(zhí)行 Objective-C 回調(diào)毫無難度,難點(diǎn)依然在于 JavaScript 代碼調(diào)用 Objective-C 之后,如何在 Objective-C 的代碼中,回調(diào)執(zhí)行 JavaScript 代碼。
目前 React Native 的做法是:在 JavaScript 調(diào)用 Objective-C 代碼時(shí),注冊要回調(diào)的 Block,并且把 BlockId 作為參數(shù)發(fā)送給 Objective-C,Objective-C 收到參數(shù)時(shí)會創(chuàng)建 Block,調(diào)用完 Objective-C 函數(shù)后就會執(zhí)行這個(gè)剛剛創(chuàng)建的 Block。
Objective-C 會向 Block 中傳入?yún)?shù)和 BlockId,然后在 Block 內(nèi)部調(diào)用 JavaScript 的方法,隨后 JavaScript 查找到當(dāng)時(shí)注冊的 Block 并執(zhí)行
實(shí)戰(zhàn)舉例
- 演示 Objective-C 是如何與 JavaScript 進(jìn)行交互的
// .h 文件
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
@interface Person : NSObject<RCTBridgeModule, RCTBridgeMethod>
@end
// .m 文件
#import "Person.h"
#import "RCTEventDispatcher.h"
#import "RCTConvert.h"
@implementation Person
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(greet:(NSString *)name)
{
NSLog(@"Hi, %@!", name);
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": @"nmae"}];
}
RCT_EXPORT_METHOD(greetss:(NSString *)name name2:(NSString *)name2 callback:(RCTResponseSenderBlock)callback)
{
NSLog(@"Hi, %@! %@!!!", name, name2);
callback(@[@[@12,@23,@34]]);
}
@end
// JavaScript 中:
Person.greet('Tadeu');
Person.greetss('Haha', 'Heihei', (events) => {
for (var i = 0; i < events.length; i++) {
console.log(events[i]);
}
});
React Native 優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
1.復(fù)用了 React 的思想,有利于前端開發(fā)者涉足移動端
2.能夠利用 JavaScript 動態(tài)更新的特性,快速迭代
3.相比于原生平臺,開發(fā)速度更快,相比于 Hybrid 框架,性能更好
缺點(diǎn)
1. 開發(fā)者依然要為 iOS 和 Android 兩個(gè)平臺提供兩套不同的代。有組件是區(qū)分平臺的,即使是共用組件,也會有平臺獨(dú)享的函數(shù)。
2. 不能做到完全屏蔽 iOS 端或 Android端,前端開發(fā)者必須對原生平臺有所了解。
3. 由于 Objective-C 與 JavaScript 之間的切換存在固定的時(shí)間開銷,所以性能必定不及原生。(比如目前的官方版本無法做到 UItableview(ListView) 的視圖重用,因?yàn)榛瑒舆^程中,視圖重用需要在異步線程中執(zhí)行,速度太慢。這也就導(dǎo)致隨著 Cell 數(shù)量的增加,占用的內(nèi)存也線性增加。)
React Native 交互原理總結(jié)
Objective-C 有 `JavaScript Core` 框架用來執(zhí)行 JavaScript 代碼。
JavaScript 通過配置表生成類,方法,參數(shù)三個(gè)元素,放入消息隊(duì)列,Objective-C獲取之后,
就可以唯一確定要調(diào)用的是哪個(gè)Objective-C函數(shù),然后調(diào)用