React Native的极简手册

安装入门

安装入门可以参考:React Native官方文档

NodeJS知识储备:参考《NodeJS入门》。(尊重知识,请购买原版)。

书籍:《React Native入门与实战》

代码示例:30天学习React Native教程

看到这里,对React Native的使用就有了一些认识了。

React and React Native and NodeJS

React是由Facebook开发出来的用于开发用户交互界面的JS库。其源码由Facebook和社区优秀的程序员维护。React带来了很多新的东西,例如组件化、JSX、虚拟DOM等。其提供的虚拟DOM使得我们渲染组件呈现非常之快,让我们从频繁操作DOM的繁重工作之中解脱。它做的工作更多偏重于MVC中的V层,结合其它如Flux等一起,你可以非常容易构建强大的应用。

React的世界里,一切都是组件。你可以构建任何直接的HTML没有的组件,例如下拉菜单、导航菜单等。同时,组件里也可以包含其它组件。每一个组件都有一个render方法,用于呈现该组件。同时,每一个组件都有属于自己的scope,从而与其它的组件界定开来,用于构建属于该组件的方法,以方便复用。JSX是基于JS的扩展,它允许你在JS里直接写HTML的代码,而不用像我们过去一样要想在JS里写HTML不得不拼接一大堆的字符串。React不直接操作DOM,频繁的操作DOM会非常影响性能和体验。React将DOM结构储存在内存中,与render方法的返回值进行比较,通过其自由的diff算法计算出不同的地方,然后反应到真实的DOM当中。也就是说,大多数情况我们渲染组件、更改组件状态等都是操作的虚拟DOM,只有在有所改变的情况下,才会反应到真实的DOM当中。React Native基于ReacJS,把 React 编程模式的能力带到移动开发,用来开发iOS和Android原生应用.

NodeJs 是基于JavaScript的,可以做为后台开发的语言. 提供了很多系统级的API,如文件操作、网络编程等. 用事件驱动, 异步编程,主要是为后台网络服务设计.React Native 借助 Node.js,即 JavaScript 运行时来创建 JavaScript 代码。

总结来说,React Native使用NodeJS来做系统处理,使用React来渲染。

构建原理

在AppDelegate.m里,找到

application:didFinishLaunchingWithOptions:

在这个方法中,主要做了几件事:

  • 定义了 JS 代码所在的位置,它在 dev 环境下是一个 URL,通过 development server 访问;在生产环境下则从磁盘读取,当然前提是已经手动生成过了 bundle 文件;
  • 创建了一个 RCTRootView 对象,该类继承于 UIView,处理程序所有 View 的最外层;
  • 调用 RCTRootView 的 initWithBundleURL 方法。在该方法中,创建了 bridge 对象。顾名思义,bridge 起着两个端之间的桥接作用,其中真正工作的是类就是大名鼎鼎的 RCTBatchedBridge。RCTBatchedBridge 是初始化时通信的核心,我们重点关注的是 start 方法。在 start 方法中,会创建一个 GCD 线程,该线程通过串行队列调度了以下几个关键的任务。

RCTRootView 用于加载 JavaScript 应用以及渲染最后的视图的。当应用开始运行的时候,RCTRootView将会从以下的URL中加载应用:

http://localhost:8081/index.ios.bundle

重新调用了你运行这个App时打开的终端窗口,它开启了一个 packager 和 server 来处理上面的请求。在 Safari 中打开那个 URL;你将会看到这个 App 的 JavaScript 代码。你也可以在 React Native 框架中找到你的代码。当你的App开始运行了以后,这段代码将会被加载进来,然后 JavaScriptCore 框架将会执行它。在程序里,它将会加载 功能 组件,然后构建出原生的 UIKit 视图。JavaScript应用运行在模拟器上,使用的是原生UI,没有任何内嵌的浏览器。应用程序会使用 React.createElement 来构建应用 UI ,React会将其转换到原生环境中。

当 UI 渲染出来后,render 方法会返回一颗视图渲染树,并与当前的 UIKit 视图进行比较。这个称之为 reconciliation 的过程的输出是一个简单的更新列表, React 会将这个列表应用到当前视图。只有实际改变了的部分才会重新绘制。即ReactJS独特的——virtual-DOM(文档对象模型,一个web文档的视图树)和 reconciliation概念。

组件的生命周期

组件的生命周期分成三个状态:

Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM

React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。

componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()

此外,React 还提供两种特殊状态的处理函数。

componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

这些方法的详细说明,可以参考官方文档

另外一个需要关注的点是,组件的style属性的设置方式不能写成

style="opacity:{this.state.opacity};"

而要写成

style={{opacity: this.state.opacity}}

这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。

JS 和 Native 交互

xcode启动后会执行 ../node_modules/react-native/packager/react-native-xcode.sh文件。脚本中主要是读取 Xcode 带过来的环境变量,同时加载 nvm 包使得 Node.js 环境可用,最后执行 react-native-cli 的命令:

$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \\
  --entry-file index.ios.js \\
  --platform ios \\
  --dev $DEV \\
  --bundle-output "$DEST/main.jsbundle" \\
  --assets-dest "$DEST"

通过此处,index.ios.js和main.jsbundle就可以使用了。

通过../react-native/local-cli/cli.js 中的 run 方法,进入/bundle/bundle.js ,由此进入了 /bundle/buildBundle.js。从js脚本中可以看出大体做了下面的工作:

  • 从入口文件开始分析模块之间的依赖关系;
  • 对 JS 文件转化,比如 JSX 语法的转化等;
  • 把转化后的各个模块一起合并为一个 bundle.js。

React Native对模块的分析和编译做了不少优化,大大提升了打包的速度,这样能够保证在 liveReload 时用户及时得到响应。

在应用程序启动之后,其中的 didFinishLaunchingWithOptions 方法会被调用,通过上面的分析,我们可以看到自己实现的页面就被加入到应用程序中了。JS 引擎,在调试环境下,对应的 Executor 为 RCTWebSocketExecutor,它通过 WebSocket 连接到 Chrome 中,在 Chrome 里运行 JS;在生产环境下,对应的 Executor 为 RCTContextExecutor,这应该就是传说中的 javascriptcore。

Native 调用 JS 是通过发送消息到 Chrome 触发执行、或者直接通过 javascriptcore 执行 JS 代码的。在 JS 端调用 Native 一般都是直接通过引用模块名,JS 把(调用模块、调用方法、调用参数) 保存到队列中;Native 调用 JS 时,顺便把队列返回过来;Native 处理队列中的参数,同样解析出(模块、方法、参数),并通过 NSInvocation 动态调用;Native方法调用完毕后,再次主动调用 JS。JS 端通过 callbackID,找到对应JS端的 callback,进行一次调用。两端都保存了所有暴露的 Native 模块信息表作为通信的基础。

JS不会主动传递数据给OC,在调OC方法时,会把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。native开发里,只在有事件触发的时候才执行代码。在React Native里,事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的。

另外,一个 Native 模块如果想要暴露给 JS,需要在声明时显示地调用 RCT_EXPORT_MODULE。宏定义了 load 方法,该方法会自动被调用,在方法中对当前类进行注册。模块如果要暴露出指定的方法,需要通过 RCT_EXPORT_METHOD 宏进行声明。

总结:整个启动过程就是,JS端先把代码大包成bundle.js传到Native端的主函数,主函数创建RCTRootView.在RCTRootView里使用GCD扫描暴露的模块,创建JS引擎,将模块信息序列化为json.此时加载JS代码,在JS引擎中执行bundle.js,将json对象反序列化保存为NativeModules对象。


JS 和 Native 的交互过程中, RCTBatchedBridge 在两端通信过程中扮演了重要的角色。

//RCTBatchedBridge.m
- (void)start
{
  dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT);

  // 异步的加载打包完成的js文件,也就是main.jsbundle,如果包文件在本地则直接加载,否则根据URL通过NSURLSession方式去下载
  [self loadSource:^(NSError *error, NSData *source) {}];

  // 同步初始化需要暴露给给js层的native模块
  [self initModules];

  //异步初始化JS Executor,也就是js引擎
  dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
[weakSelf setUpExecutor];
  });

  //异步获取各个模块的配置信息
  dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
config = [weakSelf moduleConfig];
  });

  //获取各模块的配置信息后,将这些信息注入到JS环境中
  [self injectJSONConfiguration:config onComplete:^(NSError *error) {}];

  //开始执行main.jsbundle
  [self executeSourceCode:sourceCode];
}

js为了桥接native层也引入了BatchedBridge,BatchedBridge是MessageQueue的一个实例,而且是全局唯一的一个实例。:

//BatchedBridge.js
const MessageQueue = require('MessageQueue');
const BatchedBridge = new MessageQueue(
  __fbBatchedBridgeConfig.remoteModuleConfig,
  __fbBatchedBridgeConfig.localModulesConfig,
);
//将BatchedBridge添加到js的全局global对象中,
Object.defineProperty(global, '__fbBatchedBridge', { value: BatchedBridge });
module.exports = BatchedBridge;

__fbBatchedBridgeConfig是一个全局的js变量,__fbBatchedBridgeConfig.remoteModuleConfig就是之前我们在native层导出的模块配置表.

messageQueue保存着js跟native的模块交互的所有信息。<code>_genModules</code>方法,该方法会根据config解析每个模块的信息并保存到this.RemoteModules中.<code>_genModules</code>会历遍所有的remoteModules,根据每个模块的配置信息(如何生成配置信息下面会提到)和module索引ID来创建每个模块.

react为了性能的优化,当js两次调用方法的间隔小于MIN_TIME_BETWEEN_FLUSHES_MS(5ms)时间,会将调用信息先缓存到_queue中,等待下次在一并提交给native层执行.

Navigator组件到NavigationExperimental组件

Navigator and NavigatorIOS两个都是有状态(即保存各个导航的序顺)的组件,允许你的APP在多个不同的场景(屏幕)之间管理你的导航。这两个导航管理了一个路由栈(route stack),这样就允许我们使用pop(), push()和replace()来管理状态。NavigatorIOS是使用了iOS的 UINavigationController类,而Navigator都是基于Javascript。 Navigator适用于两个平台,而NavigatorIOS只能适用于iOS. 如果在一个APP中应用了多个导航组件(Navigator and NavigatorIOS一起使用). 那么在两者之间进行导航过渡,会变得非常困难.

NavigationExperimental以一种新的方法实现导航逻辑,这样允许任何的视图都可以作为导航的视图 。它包含了一个预编异的组件NavigationAnimatedView来管理场景间的动画。它内部的每一个视图都可以有自己的手势和动画。

React Native项目已经不再维护Navigator组件而全面转向NavigationExperimental组件了。NavigationExperimental改进了Navigator组件的一下几个方面:

  • 单向数据流, 它使用reducers 来操作最顶层的state 对像,而在Navigator中,当你在子导航页中,不可能操作到app最初打开页面时的state对像,除非,一级级的通过props传递过方法名或函数名,然后在子页面中调用这些方法或者函数,来修改某个顶层的数据。

  • 为了允许存在本地和基于 js的导航视图,导航的逻辑和路由,必须从视图逻辑中独立出来。

  • 改进了切换时的场景动画,手势和导航栏

NavigationExperimental的使用:
实现方案可参考此处

具体使用也可以看这里.

推荐阅读更多精彩内容