React Native 从入门到原理
React
我想动态修改一个按钮的文字,需要这样写:
<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 事件触发的函数必须完全对应,否则就无法正确的响应事件。如果想知道一个 HTML 标签会如何被响应,我们还得跑去 JavaScript 代码中查找,这种原始的配置方式很不方便。
于是FaceBook 推出了 React 框架,这个问题得到了大幅度改善。我们可以把一组相关的 HTML 标签,也就是 app 内的 UI 控件,封装进一个组件(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 代码里面出现了 HTML 的语法”,那么恭喜你已经初步体会到 React 的奥妙了。这种语法被称为 JSX
,它是一种 JavaScript 语法拓展。JSX 允许我们写 HTML 标签或 React 标签,它们终将被转换成原生的 JavaScript 并创建 DOM。
在 React 框架中,除了可以用 JavaScript 写 HTML 以外,我们甚至可以写 CSS,这在后面的例子中可以看到。
React 是一套可以用 简洁 的语法 高效 绘制 DOM 的框架:
简洁因为我们可以暂时放下 HTML 和 CSS,只关心如何用 JavaScript 构造页面
高效是因为 React 独创了 Virtual DOM 机制。Virtual DOM 是一个存在于内存中的 JavaScript 对象,
它与 DOM 是一一对应的关系,也就是说只要有 Virtual DOM,我们就能渲染出 DOM。
当界面发生变化时,得益于高效的 DOM Diff 算法,我们能够知道 Virtual DOM 的变化,从而高效的改动 DOM,避免了重新绘制 DOM。
React 并不是前端开发的全部,它专注于 UI 部分,对应到 MVC 结构中就是 View 层。要想实现完整的 MVC 架构,还需要 Model 和 Controller 的结构。在前端开发时,我们可以采用 Flux 和 Redux 架构,它们并非框架(Library),而是和 MVC 一样都是一种架构设计(Architecture)。
如果不从事前端开发,就不用深入的掌握 Flux 和 Redux 架构,但理解这一套体系结构对于后面理解 React Native 非常重要。
React Native
移动端通过 JSON 文件传递信息,只能传递配置信息,无法表达逻辑。
而 React 在前端取得突破性成功以后,JavaScript 布道者们开始试图一统三端。他们利用了移动平台能够运行 JavaScript 代码的能力,并且发挥了 JavaScript 不仅仅可以传递配置信息,还可以表达逻辑信息的优点。
于是一个基于 JavaScript,具备动态配置能力,面向前端开发者的移动端开发框架,React Native,诞生了!
这是一个面向前端开发者的框架。它的宗旨是让前端开发者像用 React 写网页那样,用 React Native 写移动端应用。
原理概述
-
React Native 与 Hybrid 完全没有关系
- 即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码。React Native只不过是以 JavaScript 的形式告诉 Objective-C 该执行什么代码。
-
React Native 能够运行起来,全靠 Objective-C 和 JavaScript 的交互。
-
我们知道 C 系列的语言,经过编译,链接等操作后,会得到一个二进制格式的可执行文,所谓的运行程序,其实是运行这个二进制程序。
-
而 JavaScript 是一种脚本语言,它不会经过编译、链接等操作,而是在运行时才动态的进行词法、语法分析,生成抽象语法树(AST)和字节码,然后由解释器负责执行或者使用 JIT 将字节码转化为机器码再执行。整个流程由 JavaScript 引擎负责完成。
-
JavaScript 是一种单线程的语言;
-
JavaScript不具备自运行的能力,因此总是被动调用。
-
“JavaScript 线程” 实际上表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。
-
-
苹果提供了一个叫做 JavaScript Core 的框架,这是一个 JavaScript 引擎:
- JSContext 指的是 JavaScript 代码的运行环境,通过方法
evaluateScript
即可执行 JavaScript 代码获取返回结果
- JSContext 指的是 JavaScript 代码的运行环境,通过方法
// Objective-C 如何调用 JavaScript :
JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
Objective-C 与 JavaScript 交互
Objective-C 和 JavaScript 的交互总是由前者发起
由于 JavaScript Core 是一个面向 Objective-C 的框架,在 Objective-C 这一端,我们对 JavaScript 上下文知根知底,可以很容易的获取到对象,方法等各种信息,当然也包括调用 JavaScript 函数。
真正复杂的问题在于,JavaScript 不知道 Objective-C 有哪些方法可以调用。
-
React Native 解决这个问题的方案是在 Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的 模块和方法。
-
无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments 这三个元素,它们分别表示 类、方法和方法参数
-
JavaScript 解析出将调用方法的三个元素后放入到 MessageQueue 中,等待 Objective-C 拿走
-
当 Objective-C 接收到这三个元素后,就可以通过 runtime 唯一确定要调用的是哪个Objective-C函数,然后调用这个函数
上述解决方案只是一个抽象概念,可能与实际的解决方案有微小差异,比如实际上 Objective-C 这一端,并没有直接保存这个模块配置表。具体实现将在下一节中随着源码一起分析。
React Native Objective-C端源码分析
配置表的形成 (Objective-C 调用 JavaScript)
每个项目都有一个入口,然后进行初始化操作,React Native 也不例外。一个不含 Objective-C 代码的项目留给我们的唯一线索就是位于 AppDelegate 文件中的代码:
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"PropertyFinder" initialProperties:nil launchOptions:launchOptions];
用户能看到的一切内容都来源于这个 rootView,所有的初始化工作也都在这个方法内完成。
在这个方法内部,在创建 rootView 之前,React Native 实际上先创建了一个 BatchedBridge 对象
。它是 Objective-C 与 JavaScript 交互的桥梁,后续的方法交互完全依赖于它,而整个初始化过程的最终目的其实也就是创建这个桥梁对象。
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任务则是创建 BatchedBridge对象
。
BatchedBridge对象
的作用是批量读取 JavaScript 对 Objective-C 的方法调用,同时它内部持有一个 RCTJSCExecutor对象 对象
,用来执行 JavaScript 代码。
创建 BatchedBridge对象
的关键是 start 方法,start方法又分为五个步骤:
1.读取JavaScript源码
2.初始化模块信息
3.初始化 JavaScript 代码的执行器(即 RCTJSCExecutor对象)
4.生成模块列表并写入 JavaScript 端
5.执行 JavaScript 源码
方法调用
如前文所述,在 React Native 中,Objective-C 和 JavaScript 的交互都是通过传递 ModuleId、MethodId 和 Arguments 进行的。以下是分情况讨论:
调用 JavaScript 方法
调用 JavaScript 代码的核心代码如下:
// 这个函数是我们要调用 JavaScript 的中转函数。也就是说它的作用其实是处理参数,而非真正要调用的 JavaScript 函数。
// 这个中转函数接收到的参数包含了 ModuleId、MethodId 和 Arguments,然后由中转函数查找自己的模块配置表,找到真正要调用的 JavaScript 函数
- (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 转换成 Objective-C 类型*/
onComplete(objcValue, nil);
}];
}
在实际使用的时候,我们可以这样发起对 JavaScript 的调用:
// Name 和 Body 参数分别表示要调用的 JavaScript 的函数名和参数
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": @"nmae"}];
调用 Objective-C方法
-
JavaScript 解析出方法的 ModuleId、MethodId 和 Arguments 并放入到 MessageQueue 中,等待 Objective-C 拿走 (或者超时后主动发送给 Objective-C)。
-
Objective-C通过查找模块配置表找出要调用的方法,并通过 runtime 动态的调用
-
Objective-C 负责处理调用的方法是
handleBuffer
,它的参数是一个含有四个元素的数组,每个元素也都是一个数组,分别存放了 ModuleId、MethodId、Params,第四个元素目测用处不大。 -
函数内部在每一次方法调用中调用
_handleRequestNumber:moduleID:methodID:params
方法。通过查找模块配置表找出要调用的方法,并通过 runtime 动态的调用:
[method invokeWithBridge:self module:moduleData.instance arguments:params];
-
在这个方法中,有一个很关键的方法:
processMethodSignature
,它会根据 JavaScript 的 CallbackId 创建一个 Block,并且在调用完函数后执行这个 Block。
-
JavaScript闭包的回调
既然说到函数互调,那么就不得不提到回调了。对于 Objective-C 来说,执行完 JavaScript 代码再执行 Objective-C 回调毫无难度,难点依然在于 JavaScript 代码调用 Objective-C 之后,如何在 Objective-C 的代码中,回调执行 JavaScript 代码。
目前 React Native 的做法是:在 JavaScript 调用 Objective-C 代码时,注册要回调的 Block,并且把 BlockId 作为参数发送给 Objective-C,Objective-C 收到参数时会创建 Block,调用完 Objective-C 函数后就会执行这个刚刚创建的 Block。
Objective-C 会向 Block 中传入参数和 BlockId,然后在 Block 内部调用 JavaScript 的方法,随后 JavaScript 查找到当时注册的 Block 并执行
实战举例
- 演示 Objective-C 是如何与 JavaScript 进行交互的
// .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 优缺点
优点:
1.复用了 React 的思想,有利于前端开发者涉足移动端
2.能够利用 JavaScript 动态更新的特性,快速迭代
3.相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好
缺点
1. 开发者依然要为 iOS 和 Android 两个平台提供两套不同的代。有组件是区分平台的,即使是共用组件,也会有平台独享的函数。
2. 不能做到完全屏蔽 iOS 端或 Android端,前端开发者必须对原生平台有所了解。
3. 由于 Objective-C 与 JavaScript 之间的切换存在固定的时间开销,所以性能必定不及原生。(比如目前的官方版本无法做到 UItableview(ListView) 的视图重用,因为滑动过程中,视图重用需要在异步线程中执行,速度太慢。这也就导致随着 Cell 数量的增加,占用的内存也线性增加。)
React Native 交互原理总结
Objective-C 有 `JavaScript Core` 框架用来执行 JavaScript 代码。
JavaScript 通过配置表生成类,方法,参数三个元素,放入消息队列,Objective-C获取之后,
就可以唯一确定要调用的是哪个Objective-C函数,然后调用