React Native 从入门到原理
React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却寥寥无几。 本文分为两个部分:上半部分用通俗的语言解释了相关的名词,重点介绍 React Native 出现的背景和试图解决的问题。适合新手对 React Native 形成初步了解。(事实证明,女票能看懂这段) 下半部分则通过源码(0.27 版本)分析 React Native 的工作原理,适合深入学习理解 React Native 的运行机制。最后则是我个人对 React Native 的分析与前景判断。 动态配置由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题。无论采用何种方式,我们的流程总是可以归结为以下三部曲:“从 Server 获取配置 --> 解析 --> 执行native代码”。 很多时候,我们自觉或者不自觉的利用 JSON 文件实现动态配置的效果,它的核心流程是:
通过这种方法,我们实现了在后台配置 app 的展示样式。从本质上来说,移动端和服务端约定了一套协议,但是协议内容严重依赖于应用内要展示的内容,不利于拓展。也就是说,如果业务要求频繁的增加或修改页面,这套协议很难应付。 最重要的是,JSON 只是一种数据交换的格式,说白了,我们就是在解析文本数据。这就意味着它只适合提供一些配置信息,而不方便提供逻辑信息。举个例子,我们从后台可以配置颜色,位置等信息,但如果想要控制 app 内的业务逻辑,就非常复杂了。 记住,我们只是在解析字符串,它完全不具备运行和调试的能力。 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 代码中, 初识 React随着 FaceBook 推出了 React 框架,这个问题得到了大幅度改善。我们可以把一组相关的 HTML 标签,也就是 app 内的 UI 控件,封装进一个组件(Component)中,我从阮一峰的 React 教程中摘录了一段代码: var MyComponent = React.createClass({
handleClick: function() {
this.refs.myTextInput.focus();
},render: return (
<div>
<input type="text" ref="myTextInput" />
<"button" value="Focus the text input" onClick={.handleClick} />
</div>
);
}
});
如果你想问:“为什么 JavaScript 代码里面出现了 HTML 的语法”,那么恭喜你已经初步体会到 React 的奥妙了。这种语法被称为 JSX,它是一种 JavaScript 语法拓展。JSX 允许我们写 HTML 标签或 React 标签,它们终将被转换成原生的 JavaScript 并创建 DOM。 在 React 框架中,除了可以用 JavaScript 写 HTML 以外,我们甚至可以写 CSS,这在后面的例子中可以看到。 理解 React前端界总是喜欢创造新的概念,仿佛谁说的名词更晦涩,谁的水平就越高。如果你和当时的我一样,听到 React 这个概念一脸懵逼的话,只要记住以下定义即可:
上文已经解释过了何谓“简洁的语法”,因为我们可以暂时放下 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分别介绍完了移动端和前端的背景知识后,本文的主角——React Native 终于要登场了。 融合前面我们介绍了移动端通过 JSON 文件传递信息的不足之处:只能传递配置信息,无法表达逻辑。从本质上讲,这是因为 JSON 毕竟只是纯文本,它缺乏像编程语言那样的运行能力。 而 React 在前端取得突破性成功以后,JavaScript 布道者们开始试图一统三端。他们利用了移动平台能够运行 JavaScript 代码的能力,并且发挥了 JavaScript 不仅仅可以传递配置信息,还可以表达逻辑信息的优点。 当痛点遇上特点,两者一拍即合,于是乎:
看到了么,这是一个面向前端开发者的框架。它的宗旨是让前端开发者像用 React 写网页那样,用 React Native 写移动端应用。这就是为什么 React Native 自称:
而非很多跨平台语言,项目所说的:
React Native 希望前端开发者学习完 React 后,能够用同样的语法、工具等,分别开发安卓和 iOS 平台的应用并且不用一行原生代码。 如果用一个词概括 React Native,那就是:Native 版本的 React。 原理概述React Native 不是黑科技,我们写的代码总是以一种非常合理,可以解释的方式的运行着,只是绝大多数人没有理解而已。接下来我以 iOS 平台为例,简单的解释一下 React Native 的原理。 首先要明白的一点是,即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码。总之,JavaScript 只是辅助,它只是提供了配置信息和逻辑的处理结果。React Native 与 Hybrid完全没有关系,它只不过是以 JavaScript 的形式告诉 Objective-C 该执行什么代码。 其次,React Native 能够运行起来,全靠 Objective-C 和 JavaScript 的交互。对于没有接触过 JavaScript 的人来说,非常有必要理解 JavaScript 代码如何被执行。 我们知道 C 系列的语言,经过编译,链接等操作后,会得到一个二进制格式的可执行文,所谓的运行程序,其实是运行这个二进制程序。 而 JavaScript 是一种脚本语言,它不会经过编译、链接等操作,而是在运行时才动态的进行词法、语法分析,生成抽象语法树(AST)和字节码,然后由解释器负责执行或者使用 JIT 将字节码转化为机器码再执行。整个流程由 JavaScript 引擎负责完成。 苹果提供了一个叫做 JavaScript Core 的框架,这是一个 JavaScript 引擎。通过下面这段代码可以简单的感受一下 Objective-C 如何调用 JavaScript 代码: JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
这里的 JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。 Objective-C 与 JavaScript 交互提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解。虽然其中不少细节都已经过时,但是整体的思路值得学习。 本节主要分析 Objective-C 与 JavaScript 交互时的整理逻辑与流程,下一节将通过源码来分析具体原理。 JavaScript 调用 Objective-C由于 JavaScript Core 是一个面向 Objective-C 的框架,在 Objective-C 这一端,我们对 JavaScript 上下文知根知底,可以很容易的获取到对象,方法等各种信息,当然也包括调用 JavaScript 函数。 真正复杂的问题在于,JavaScript 不知道 Objective-C 有哪些方法可以调用。 React Native 解决这个问题的方案是在 Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 再次重申,上述解决方案只是一个抽象概念,可能与实际的解决方案有微小差异,比如实际上 Objective-C 这一端,并没有直接保存这个模块配置表。具体实现将在下一节中随着源码一起分析。 闭包与回调既然说到函数互调,那么就不得不提到回调了。对于 Objective-C 来说,执行完 JavaScript 代码再执行 Objective-C 回调毫无难度,难点依然在于 JavaScript 代码调用 Objective-C 之后,如何在 Objective-C 的代码中,回调执行 JavaScript 代码。 目前 React Native 的做法是:在 JavaScript 调用 Objective-C 代码时,注册要回调的 Block,并且把 Objective-C 会向 Block 中传入参数和 图解好吧,如果你是新手,并且坚持读到了这里,估计已经懵逼了。不要担心,与 JavaScript 的交互确实不是一下子能够完全理清楚的,你可以先参考这个示意图: 注:
React Native 源码分析要想深入理解 React Native 的工作原理,有两个部分有必要阅读一下,分别是初始化阶段和方法调用阶段。 为了提炼出代码的核心含义,我会在不改变代码意图的基础上对它做一些删改,以便阅读。 写这篇文章是,React Native 还处于 0.27 版本,由于在 1.0 之前的变动幅度相对较大,因此下面的源码分析很可能随着 React Native 的演变而过时。但不管何时,把下面的源码读一遍都有助于你加深对 React Native 原理的理解。 初始化 React Native 每个项目都有一个入口,然后进行初始化操作,React Native 也不例外。一个不含 Objective-C 代码的项目留给我们的唯一线索就是位于 RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"PropertyFinder"
initialProperties:nil
launchOptions:launchOptions];
用户能看到的一切内容都来源于这个 在这个方法内部,在创建 初始化方法的核心是 创建
我们逐个分析每一步完成的操作: 读取 JavaScript 源码这一部分的具体代码实现没有太大的讨论意义。我们只要明白,JavaScript 的代码是在 Objective-C 提供的环境下运行的,所以第一步就是把 JavaScript 加载进内存中,对于一个空的项目来说,所有的 JavaScript 代码大约占用 1.5 Mb 的内存空间。 需要说明的是,在这一步中,JSX 代码已经被转化成原生的 JavaScript 代码。 初始化模块信息 这一步在方法 #define RCT_EXPORT_MODULE(js_name)
RCT_EXTERN void RCTRegisterModule(Class);
+ (NSString *)moduleName { return @#js_name; }
+ (void)load { RCTRegisterModule(self); }
这样,这个类在 void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
RCTModuleClasses = [NSMutableArray new];
});
[RCTModuleClasses addObject:moduleClass]; }
因此,React Native 可以通过 for (Class moduleClass in RCTGetModuleClasses()) {
RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self];
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
可以想见, 这个对象保存了 Module 的名字,常量等基本信息,最重要的属性是一个数组,保存了所有需要暴露给 JavaScript 的方法。 暴露给 JavaScript 的方法需要用 - (NSArray<id<RCTBridgeMethod>> *)methods{
unsigned int methodCount;
Method *methods = class_copyMethodList(object_getClass(_moduleClass),&methodCount); // 获取方法列表
for (int i = 0; i < methodCount; i++) {
RCTModuleMethod *moduleMethod = /* 创建 method */
[_methods addObject:moduleMethod];
}
}
return _methods;
}
因此 Objective-C 管理模块配置表的逻辑是:Bridge 持有一个数组,数组中保存了所有的模块的 初始化 JavaScript 代码的执行器,即
|