ReactJS到React-Native,架构原理概述
Author:zhoulujun Date:
React是一个纯JS的UI库,只能干HTML/CSS/JS 提供的Web服务(新的H5 API不一定支持), React-Native厉害在于它能打通JS和Native Code, 让JS能够调用丰富的原生接口,充分发挥硬件的能力, 实现非常复杂的效果,同时能保证效率和跨平台性。
在一定程度上,React Native和NodeJS有异曲同工之妙。它们都是通过扩展JavaScript Engine, 使它具备强大的本地资源和原生接口调用能力,然后结合JavaScript丰富的库和社区和及其稳定的跨平台能力,把javascript的魔力在浏览器之外的地方充分发挥出来
因为种种原因,浏览器里面的JS代码是不允许调用自定义的原生代码的,而React又是为浏览器JS开发的一套库,所以,比较容易理解的事实是React是一个纯JS库,它封装了一套Virtual Dom的概念,实现了数据驱动编程的模式,为复杂的Web UI实现了一种无状态管理的机制, 标准的HTML/CSS之外的事情,它无能为力。调用原生控件,驱动声卡显卡,读写磁盘文件,自定义网络库等等,这是JS/React无能为力的。
驱动硬件的能力决定能一个软件能做多大的事情,有多大的主控性。研究过操作系统底层东西或者汇编的同学明白,我们大部分时候写的代码是受限的代码,很多特权指令我们是没法使用的,很多设备我们是不允许直接驱动的。我们现在的编程里面几乎已经没有人提中断了,没有中断,硬件的操作几乎会成为一场灾难.
Virtual DOM
在React 中,Virtual DOM 就像是一个中间层,介于开发者描述的视图与实际在页面上渲染的视图之间。为了在浏览器上渲染出可交互的用户界面,开发者必须操作浏览器的文档对象模型(DOM,document object model)。这个操作代价昂贵,对DOM 的过度操作将会给性能带来严重的影响。React 维护了一个内存版本的DOM,通过计算得出必要的最小操作并重新渲染。
对于Web 环境的React 而言,大多数的开发者认为Virtual DOM 的出现主要是为了优化性能。Virtual DOM 确实能提升性能,但它主要的潜力在于提供了强大的抽象能力。在开发者的代码与实际的渲染之间加入一个抽象层,这带来了很多可能性。
对于 React Native ,React Native 调用Objective-C 的API 去渲染iOS 组件,调用Java API 去渲染Android 组件,而不是渲染到浏览器DOM 上。
React-Native不使用HTML来渲染App,但是提供了可代替它的类似组件。这些React-Native组件映射到渲染到App中的真正的原生iOS和Android UI组件,意味着你不能重用之前使用ReactJS渲染的HTML, SVG或Canvas任何库。
桥接令这一切成为可能,它使得React 可调用宿主平台开放的UI 组件。React 组件通过render 方法返回了描述界面的标记代码。如果是在Web 平台上,React 最终将把标记代码解析成浏览器的DOM;而在React Native 中,标记代码会被解析成特定平台的组件,例如<View> 将会表现为iOS 平台上的UIView。
React Native,生命周期与React 基本相同,但渲染过程有一些区别,因为React Native 依赖于桥接,正如先前图所示。JavaScript 通过桥接的解析,间接调用宿主平台的基础API 和UI 元素(也就是Objective-C 或Java)。由于React Native 不在UI 主线程运行,它可以在不影响用户体验的前提下执行这些异步调用。
组件编写视图
当编写Web 环境的React 时,视图最终需要渲染成普通的HTML 元素(<div>、<p>、<span>、<a> 等)。而在React Native 中,所有的元素都将被平台特定的React 组件所替换
React与React Native基础元素的比较
React Native | Android View | iOS View | Web Analog | Description |
---|---|---|---|---|
<View> | <ViewGroup> | <UIView> | A non-scrollling
| A container that supports layout with flexbox, style, some touch handling, and accessibility controls |
<Text> | <TextView> | <UITextView> | <p> | Displays, styles, and nests strings of text and even handles touch events |
<Image> | <ImageView> | <UIImageView> | <img> | Displays different types of images |
<ScrollView> | <ScrollView> | <UIScrollView> | <div> | A generic scrolling container that can contain multiple components and views |
<TextInput> | <EditText> | <UITextField> | <input type="text"> | Allows the user to enter text |
RNTester 应用是一个打包的标准React Native 示例(facebook/react-nativetree/master/RNTester),可以让你查看它所支持的所有UI 元素,建议你体验一下其中包含的各种元素。除此之外,它还讲解了许多关于样式和交互的知识。
平台特定的元素和API 在官方文档中有特殊的标签,通常使用平台名称作为后缀,例如<TabBarIOS> 和<ToolbarAndroid>。
这些组件因平台而不同,因此在使用React Native 时,如何组织你的组件变得尤为重要。在Web 环境的React 中,我们通常混合各种React 组件,有的组件控制逻辑及其子组件,而有的则渲染原生标记。在使用React Native 时,如果你想复用代码,那么这些组件的抽象分离就至关重要。当然,如果一个组件渲染<DatePickerIOS> 元素,那它显然不能在Android 平台复用了。不过,如果一个组件封装的是关联逻辑,那就可以被复用。因此,视图组件可以根据平台进行替换选择。如果你乐意的话,还可以为组件设计平台特定的版本,例如picker.ios.js 和picker.android.js。
React Native 渲染 在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中
在 React Native 框架中,JSX 源码通过 React Native 框架编译后,通过对应平台的 Bridge 实现了与原生框架的通信。如果我们在程序中调用了 React Native 提供的 API,那么 React Native 框架就通过 Bridge 调用原生框架中的方法。 因为 React Native 的底层为 React 框架,所以如果是 UI 层的变更,那么就映射为虚拟 DOM 后进行 diff 算法,diff 算法计算出变动后的 JSON 映射文件,最终由 Native 层将此 JSON 文件映射渲染到原生 App 的页面元素上,最终实现了在项目中只需要控制 state 以及 props 的变更来引起 iOS 与 Android 平台的 UI 变更。 编写的 React Native代码最终会打包生成一个 main.bundle.js 文件供 App 加载,此文件可以在 App 设备本地,也可以存放于服务器上供 App 下载更新,
核心组件和API:https://www.reactnative.cn/docs/components-and-apis
样式布局与Yoga
Yoga C语言写的一个 CSS3/Flexbox 的跨平台 实现的Flexbox布局引擎
Yoga 通过实现许多设计师熟悉的 API 并在不同平台上向开发人员开放。 Facebook引领着移动开源风向,这次它对布局出手了,推出了Yoga开源项目,意在打造一个跨iOS、Android、Windows平台在内的布局引擎,兼容Flexbox布局方式,让界面布局更加简单。
利用YOGA我们可以:
只写一次布局,就可以得到在不同端上的布局展示。
动态更改view的布局
目前已经被用于在React Native 和 Weex 等开源项目中
但是Yoga只实现了W3C标准的一个子集,所以样式方面,也只有随着Yoga了
DOM和Styles
大多数组件都类似HTML,例如View组件跟div标签就很类似,Text组件类似于p标签。
<View style={styles.container}> <Text style={styles.intro}>Hello world!</Text> </View>
为了给React-Native组件加上样式,你需要在JavaScript中添加样式表。
React 和宿主平台之间的桥接包含了一个缩减版CSS 子集的实现。这个CSS 子集主要通过flexbox 进行布局,做到了尽量简单化,而不是去实现所有的CSS 规则。
React Native 也坚持使用内联样式,通过JavaScript 对象进行样式组织。React 团队先前也提倡在Web 环境的React 中使用内联样式。
相对于样式表来说,使用样式对象可能需要一些思维上的调整,从而改变你编写样式的方法。然而,在React Native 中,这是一个实用的转变。
Flexbox构建响应式App的最佳选择——CSS中的表现不太一致,React-Native并不是为web元素而生,不能像web 应用在html里面使用CSS
这里还是体现了Weex优势
维度 | React Native | Weex |
---|---|---|
支持 | Alibaba | |
思想 | Learn once, write anywhere | Write once, run anywhere |
编写方式 | 需针对iOS、Android编写2份代码 | 只需要编写一份代码,即可运行在Web、iOS、Android上 |
JS引擎 | JSCore | V8 |
框架 | React.js组件化,数据绑定 Virtual DOM JSX模板学习使用有一定的成本 | Vue.JS 组件化,数据绑定 Virtual DOM 模板就是普通的html,数据绑定使用mustache风格,样式直接使用css |
异步 | 提供了Promise的支持 | 只支持callback |
扩展 | 不同平台可自由扩展 | 为了保证各平台的一致性,一次扩展得在各个平台都实现 |
组件 | 除了自带的,还有js.coach上社区贡献的,比较丰富 | 基本靠平台提供 |
性能 | 优 | 更优秀 |
社区 | 非常成熟和活跃 | 开源较晚,社区处于成长期 |
上手难度 | 困难 | 容易 |
不过,个人还是推荐react
动画和手势
在React-Native中你需要通过JavsScript以一种全新的方式让不同的组件动起来。推荐的方式是使用React-Native提供的Animated API。
为了跟用户手势很好的交互,React-Native提供了类似JavaScript的touch事件 web API,叫做PanResponder。
PanResponder提供了一系列function来捕捉用户的触摸事件,例如onPanResponderGrant (touchstart), onPanResponderMove(touchmove) 或onPanResponderRelease (touchend)。通过这些function可以得到原生事件和手势状态信息,如所有的touch、位置以及滑动距离,速度和触摸中心等。
导航
React-Native提供的Navigator组件。
应该坚持使用Navigator组件,除非你开发了一个规模庞大的移动App,需要很多的页面,或者你害怕在某些时候会混乱。你也可以看下NavigatorExperimental这个组件,但在我看来,它还不适用于生成环境。
宿主平台API
Web 环境的React 与React Native 最大的不同,应该就在于宿主平台的API 了。
在Web 中,我们通常要处理采纳标准的不一致和碎片化所引起的问题,并且大多数浏览器只支持部分核心的特性。然而在React Native 中,平台特定的API 在提供优秀原生的用户体验方面发挥了巨大的作用。比如React Native 提供了和 web 标准一致的Fetch API,用于满足开发者访问网络的需求。
当然,要考虑的方面还有很多。API 囊括了许多功能,从数据存储到地理服务,以及操控硬件设备(如摄像头)等。非常规平台上的API 会更有趣,例如,React Native 和虚拟现实头盔之间的API 会是什么样的呢?
React Native 采用了 JavaScriptCore 作为 JS VM,中间通过 JSON 文件与 Bridge 进行通信。而如果在使用 Chrome 浏览器进行调试时,那么所有的 JavaScript 代码都将运行在 Chrome 的 V8 引擎中,与原生代码通过 WebSocket 进行通信。
解整套框架里的一些重要角色,如下所示:
ReactContext:ReactContext继承于ContextWrapper,是ReactNative应用的上下文,通过getContext()去获得,通过它可以访问ReactNative核心类的实现。
ReactInstanceManager:ReactInstanceManager是ReactNative应用总的管理类,创建ReactContext、CatalystInstance等类,解析ReactPackage生成映射表,并且配合ReactRootView管理View的创建与生命周期等功能。
CatalystInstance:CatalystInstance是ReactNative应用Java层、C++层、JS层通信总管理类,总管Java层、JS层核心Module映射表与回调,三端通信的入口与桥梁。
NativeToJsBridge:NativeToJsBridge是Java调用JS的桥梁,用来调用JS Module,回调Java。
JsToNativeBridge:JsToNativeBridge是JS调用Java的桥梁,用来调用Java Module。
JavaScriptModule:JavaScriptModule是JS Module,负责JS到Java的映射调用格式声明,由CatalystInstance统一管理。
NativeModule:NativeModule是ava Module,负责Java到Js的映射调用格式声明,由CatalystInstance统一管理。
JavascriptModuleRegistry:JavascriptModuleRegistry是JS Module映射表,NativeModuleRegistry是Java Module映射表
以上便是整套框架中关键的角色,值得一提的是,当页面真正渲染出来以后,它实际上还是Native代码,React Native的作用就是把JavaScript代码映射成Native代码以及实现两端
的通信,所以我们在React Native基础框架搭建的过程中,指导思路之一就是弱化Native与RN的边界与区别,让业务开发组感受不到两者的区别,从而简化开发流程。
React-Native与原生的交互
与Android通信
1. 通过原生Module进行交互((Native Modules))
封装原生Module:将Android原生功能封装成中间件的形式,这个中间件需要继承ReactContextBaseJavaModule类,并重写getName()方法以返回模块名称,使得JavaScript端可以通过NativeModules访问该模块。
注册自定义Package:创建一个实现了ReactPackage接口的自定义Package,在createNativeModules方法中返回包含自定义Module实例的列表,以完成注册。然后,将这个自定义Package实例注册到ReactNativeHost中。
在JavaScript端使用:通过NativeModules访问自定义的Module,并调用其提供的方法。
2. 通过原生View进行交互( (Native Views))
创建自定义ViewManager:在Android中,可以通过创建自定义的ViewManager来封装原生View,ViewManager负责创建、更新和销毁原生View。
注册自定义ViewManager:与注册自定义Module类似,需要在自定义Package的createViewManagers方法中返回包含自定义ViewManager的列表。
在JavaScript端使用:通过requireNativeComponent或React.createNativeComponent引入并使用自定义的View。
3. 通过发送事件进行交互 (Events)
原生向RN发送事件:Android原生代码可以使用DeviceEventManagerModule的emit方法向JavaScript端发送事件,JavaScript端可以通过DeviceEventEmitter的addListener方法监听这些事件。
RN向原生发送事件:虽然不常见,但JavaScript端也可以通过特定的机制(如模块方法调用)向原生代码发送“请求”,原生代码在响应这些请求时可以视为接收到了来自JavaScript的事件。
总体来说:
React Native到Android的通信通常通过原生模块实现,其次是通过事件,从React Native发送数据到Android,可以通过定义事件和回调函数的方式进行通信。
Android到React Native的通信通常通过回调函数实现(在Android原生代码中调用JavaScript函数,并传递参数给JavaScript代码。,如果异步,则使用promise)。
与iOS通信:
React Native与iOS通信方式与上面的Android种类大抵相同。就iOS,更详细地说明下
通讯机制Eg
React Native使用的是Android或iOS的本地控件来做UI渲染的,因此我们需要 UIKit 等原生框架,需要调用 Objective-C 代码或者Java代码,同时我们也需要在原生代码中运行js代码,比如UI控件上注册的事件,这就需要在js端和原生端有对应的通讯机制。
我们都知道 JavaScript 是一种脚本语言,它不会经过编译、链接等操作,而是在运行时才动态的进行词法、语法分析,生成抽象语法树(AST)和字节码,然后由解释器负责执行或者使用 JIT 将字节码转化为机器码再执行。整个流程由 JavaScript 引擎负责完成的,IOS提供了一个叫做 JavaScript Core 的框架,这是一个 JavaScript 引擎。通过下面这段代码可以简单的感受一下 Objective-C 如何调用 JavaScript 代码的:
JSContext 指的是 JavaScript 代码的运行环境,通过 evaluateScript 即可执行 JavaScript 代码并获取返回结果。
JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用,Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。
交互流程
在 React Native 中,Objective-C 和 JavaScript 的交互都是通过传递 ModuleId、MethodId 和 Arguments 进行的。Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments 这三个元素,它们分别表示类、方法和方法参数,当 Objective-C 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。Objective-C 和 JavaScript 的交互总是由Objective-C发起的。Object-C与js的交互是通过各端的Bridge和ModuleConfig来进行的,实际过程可分为两个阶段:初始化阶段和方法调用阶段。
初始化 React Native
在RN(ios)项目中都会有 AppDelegate.m 这个文件,文件有如下代码:
用户能看到的一切内容都来源于这个 RootView,所有的初始化工作也都在这个方法内完成。在这个方法内部,在创建 RootView 之前,React Native 实际上先创建了一个 Bridge 对象。它是 Objective-C 与 JavaScript 交互的桥梁,后续的方法交互完全依赖于它,而整个初始化过程的最终目的其实也就是创建这个桥梁对象。
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任务则是创建 BatchedBridge。BatchedBridge 的作用是批量读取 JavaScript 对 Objective-C 的方法调用,同时它内部持有一个 JavaScriptExecutor,顾名思义,这个对象用来执行 JavaScript 代码。创建 BatchedBridge 的关键是 start 方法,它可以分为五个步骤:
读取 JavaScript 源码
JavaScript 的代码是在 Objective-C 提供的环境下运行的,所以第一步就是把 JavaScript 加载进内存中,对于一个空的项目来说,所有的 JavaScript 代码大约占用 1.5 Mb 的内存空间。在这一步中,JSX 代码已经被转化成原生的 JavaScript 代码。
初始化模块信息
主要任务是找到所有需要暴露给 JavaScript 的类(Module)
初始化 JavaScript 代码的执行器,即 RCTJSCExecutor 对象
初始化JavaScript代码执行器,同时向 JavaScript 上下文中添加了一些 Block(Object-c中对闭包的实现) 作为全局变量。
Block--nativeRequireModuleConfig : 它在 JavaScript 注册新的模块时调用:
Block--nativeFlushQueueImmediate:一般情况下,Objective-C 会定时、主动的调用JS放到MessageQueue 中的方法,实际上(由于卡顿或某些特殊原因),JavaScript 也可以主动调用 Objective-C 的方法,目前,React Native 的逻辑是,如果消息队列中有等待 Objective-C 处理的逻辑,而且 Objective-C 超过 5ms 都没有来取走,那么 JavaScript 就会主动调用 Objective-C 的方法。
请牢牢记住这个 5ms,它告诉我们 JavaScript 与 Objective-C 的交互是存在一定开销的,不然就不会等待而是每次都立刻发起请求。其次,这个时间开销大约是毫秒级的,不会比 5ms 小太多,否则等待这么久就意义不大了。
生成模块列表并写入 JavaScript 端
让JavaScript 获取所有模块的名字,作为一个全局变量存储
执行 JavaScript 源码
运行代码时,第三步中所添加的 Block(nativeRequireModuleConfig ) 就会被执行,从而向 JavaScript 端写入配置信息。
方法调用
OC调用 JS代码
OC不会直接调用实际的js函数,而是会去调用维系的中转函数,中转函数接收到 的参数包含了 ModuleId、MethodId 和 Arguments,就可以查找自己的模块配置表,找到真正要调用的 JavaScript 函数。
JS调用OC代码
在调用 Objective-C 代码时,JavaScript 会解析出方法的 ModuleId、MethodId 和 Arguments 并放入到 MessageQueue 中,等待 Objective-C 主动拿走,或者超时后主动发送给 Objective-C。
函数内部在每一次方调用中查找模块配置表找出要调用的方法,并通过 runtime 动态的调用。
参考文章:
React Native for Android 原理分析与实践:实现原理 https://juejin.im/post/5a6460f8f265da3e4f0a446d
翻译 | 从 ReactJS 到 React-Native—两者的主要差异是什 https://zhuanlan.zhihu.com/p/29179261
React-Native简介与运行原理解析(Eg:ios) https://www.jianshu.com/p/82a28c8b673b
https://www.zhihu.com/question/30466658/answer/656472181
转载本站文章《 ReactJS到React-Native,架构原理概述》,
请注明出处:https://www.zhoulujun.cn/html/webfront/AppDev/ReactNative/8475.html