RN 与native 的交互javascript
0>>> React 的渲染機制php
1>>> react-native 渲染原理 前端
2>>> react-native 如何与原生通讯java
3>>> 如何封装一个原生视图组件node
4>>> react-native 的线程管理react
RN的本质是利用 js 调用 native 端的组件, 从而实现相应的功能android
// react ios
React 与 React native 的原理是相同的,都是由 javascript 实现的虚拟DOM 来驱动界面 View 的渲染,只不过 React.js 驱动 HTML DOM 的渲染, RN 是驱动 ios/android 原生组件的渲染git
都知道 React native 是虚拟 DOM 机制,是存在于内存中的 JavaScript 对象对应着真实的 DOM 节点,操做DOM 很慢,但操做JavaScript 很快,因为采用diff算法只渲染相比以前改变的部分并非所有渲染,因此更快, setState 时至关于建立了一个影子 DOM 而后比较新旧两个 DOM 的区别触发 render 函数,从新渲染须要改变部分的DOM节点github
React 是 fb 推出的一个 js前端框架, 采用组件化方式简化了 web 开发
(1)DOM : 每一个 HTML 页面能够当作一个 DOM 能够把 HTML标签和 js 单独封装到一个组件类中,便于复用,方便开发
(2)更高效 原生的 web 刷新 DOM, 须要刷新整个页面 react 只刷新部分页面
(3) 首创了 Virtual DOM 机制, 是一个存在于内存中的 js对象,之因此很是快是由于他从不直接操做 DOM, 它和 DOM 是一一对应的关系, 当页面发生变化时, react 会利用 DOM diff 算法, 把有变化的 DOM 进行刷新
由当前状态决定 UI 界面 状态发生变化时 由 diff算法决定刷新哪些 DOM 节点, 虚拟DOM是在DOM的基础上创建了一个抽象层,对数据和状态所作的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到DOM中。
在React中,render执行的结果获得的并非真正的DOM节点,而仅仅是JavaScript对象,称之为虚拟DOM
逐层进行节点比较 若是上面的同级节点不一样,该节点及其子节点会被彻底删除掉,不会进行进一步的递归比较,这样只须要对 DOM 树进行一次遍历 ,便能完成整个DOM 树的比较
DOM 结构的转换
好比这种正常的逻辑: A.parent.remove(A) D.append(A)
对于 React 只考虑同层的节点的位置变换 不一样层的只有简单的建立和删除, 当发现节点中 A 不见了 ,直接销毁 A 新建一个 A 节点做为 D 的子节点
A.destroy() A = new A() A.append(new B()) A.append(new C()) D.append(A)
同级的列表节点的比较
如图: 在 JQuery 中 $(B).after(F)直接插入一个节点就行了
在 React 只能告诉新的页面是 ABFCDE组成.由 diff算法完成更新 他只能把 A 更新为 A B 更新为 B C 更新为 F 这样
若是给每一个节点一个惟一标识的 key, React 就能够找到正确的位置去插入新的节点,这也是为何项目中有些列表常常报警告缺乏 key, 也是潜在的性能问题, 虽然不影响运行
对于相同类型的元素, React 会先查看二者的属性差别, 而后保留相同的底层 DOM 节点,仅仅去更新那些被修改的属性好比只修改了 width color 等 处理完 根DOM节点后, 会根据上面的判断逻辑对子节点进行递归扫描
须要注意的两点:
1> 该算法不会去尝试匹配那些不一样组件类型的子元素树。 若是你看到本身在返回类似输出结果的两个组件类型之间来来回回,你可能须要把它们改成相同的类型组件。
2> key属性应该是稳定,可预测和惟一的。 不稳定的键(如使用Math.random()生的key)将致使许多组件实例和DOM节点进行没必要要地重复建立,这可能致使子组件中的性能下降和state丢失。
React.render 渲染流程
1>>> React.render() 将咱们的 element 根虚拟节点渲染到 container 元素中
2>>> 根据 element 的类型不一样, 分别实例化 ReactDOMTextComponnet ReactDOMComponent ReactCompositeComponent类, 这些类用来管理 ReactElement ,负责将不一样的 ReactElement 转化为 DOM, 并更新 DOM
3>>> ReactCompositeComponent 实例调用 mountComponent 方法后内部调用render 方法, 返回了 DOM Elements
React 的更新机制
React Element 一个描述 DOM 节点或 component 实例的字面级对象, 它包含一些信息,组件的 type 和 props, 就像一个描述 DOM 节点的元素(虚拟节点), 能够经过 React.creatElement 方法建立
reactClass 平时咱们所建立的 component 组件(类或函数) ReactClass 实例化后调用 render 函数可返回 DOMElement
调用 React.render 方法, 将 element根虚拟节点渲染到 container 元素中, 根据 element 的类型不一样,分别实例化 ReactDOMTextComponent ReactDOMComponent ReactCompositeComponent 类, 这些类用来管理 ReactElement 负责将不一样的 ReactElement 转化为 DOM 并更新 DOM , ReactCompositeComponent 实例调用 mountComponent 方法后内部调用 render 方法, 返回 DOMElement
总结: (1)节点之间的比较 节点 node .包括两种类型 一个是 React 组件 一个是 HTML 的 DOM
节点类型不一样: 直接使用新的替换旧的
类型相同: 样式不一样,修改样式便可,在 React 里样式并非纯粹的字符串也是一个对象,修改完当前节点以后,递归处理子节点
组件类型相同: 使用 React 机制处理,通常使用新的 props 替换旧的 props, 并在以后调用组件的 componentWill /DidRecieveProps 方法, 以前的组件的 render 方法会被调用\
(2)列表之间的比较
一列节点中有一个发生变化, React 并无什么好的办法处理,循环两个列表,找出不一样是惟一的处理方法
可是,下降这个算法的难度的办法是在生成一个列表时给每个节点一个惟一的 key,就能够方便的找出哪一个节点发生变化
// React component的生命周期函数
装载组件
更新组件状态
存在期主要是用来处理与用户的交互,如:点击事件,都比较简单,也不是很经常使用,具体有如下几个过程:
卸载(删除)组件 销毁期,用于清理一些无用的内容,如:点击事件Listener 定时器的移除
上面函数的调用顺序是:
建立时
更新时
//react-native 是如何作到js与 oc 交互的 JavascriptCore
React native 会在一开始生成两个模块配置表, js按照整个表就能够找到须要调用的方法
iOS 是经过JavaScriptCore 与 oc交互
react naive 启动流程
React -native 可以在 iOS 和安卓设备上运行起来, 是由于 rn 与 native 之间有一种交互. javascriptCore 引擎, js告诉 oc须要执行什么, iOS 会本身去调用 UIKit 等框架绘制界面
1 建立 RCTRootView
2 建立 RCTBridge 桥接对象 管理 js与 oc的交互
3 建立 RCTBatchedBridge 批量桥接对象
4 执行 [RCTBatchedBridge loadSource] 加载 js 源码
5 执行 [RCTBatchedBridge initModulesWithDispatchGroup] 建立模块配置表
6 执行 [RCTJSExcutor injectJSONText] 往 js 中插入 OC 模块表
7 执行完 js代码 回调 OC 调用 OC 中的组件
8 完成渲染
RCTBridge 是负责双方通讯的桥梁, 真正起做用的是RCTBatchedBridge类, 能够看一下他都作了什么?
当建立完模块的实例对象以后,会将该实例保存到一个RCTModuleData对象中,RCTModuleData里包含模块的类名,名称,方法列表,实例对象、该模块代码执行的队列以及配置信息等,js层就是根据这个对象来查询该模块的相关信息。
// react-native 加载 js源码流程
// react-native UI控件渲染流程
用户能看到的一切内容都源于 RootView, 实际上在建立 RootView 以前, RN 先建立了一个 Bridge 对象. 他是 OC与 JS 交互的桥梁, 整个 RN 的初始化过程其实也就是在建立整个桥梁对象
初始化方法的核心是 setUp 方法, setUp 方法主要任务是建立BatchedBridge, BatchedBridge的做用是批量读取 js对 OC 的方法调用,建立BatchedBridge的关键是 start 方法, 分为5个步骤:
1>> 读取 js源码
2>> 初始化模块信息
3>> 初始化 JS 代码执行器 RCTJSCExecutor 对象
4>> 生产模块列表并写入 JS 端
5>> 执行 js 源码 调用 OC
在调用 OC 以前, JS 会解析出方法的 MoudleID MethodID Argument 并放入 MessageQueue. 等待 OC 拿走,或者超时后主动发送给 OC
OC负责调用的方法是 handleBuffer, 他的参数是一个含有四个元素的数组, 每一个元素也是一个数组,分别存放 moudleID methodID params , 函数内部在每一次调用方法的时候调用_ handleRequestNumber: MoudleID: MethodID: params: 方法, 经过查找模块配置表找出要调用的方法, 并经过 runtime 动态调用 [method invokeWithBridge:self moudle:moudleDate.instance arguments:params] processMethodSignature ,他会根据 js 的 callbackID 建立一个 block, 并在调用完函数以后执行这个 block
1. [RCTRootView runApplication] 通知 js 运行 APP
2. [RCTBatchedBridge _processResponse:json error:error] 处理执行完js 代码返回的响应 , 包含须要添加多少个子控件的信息
3 . [RCTBatchedBridge batchDidComplete] 批量桥接对象调用批量处理完成方法
4 . [RCTUIManager batchDidComplete] RCTUIManager 调用批量处理完成的方法,就会开始去加载 rootView 的子控件
5 . [RCTUIManager creatView: viewName :rootTags: props:] 经过 js 执行 OC 代码, 让 UIManager 建立子控件 View
6 .[RCTUIManager _layoutAndMount] 布局子组件
7 .[RCTUIManager setChildren: reactTags: ] 给 RCTRootView 对应的 RCTRootShadowView 设置子组件
8 .[RCTRootShadowView insertReactSubView:view atIndex:index++] 遍历子组件数组, 给 RCTRootShadowView 插入全部子控件
9.[RCTShadowView processUpdatedProperties:parentProperties] 处理保存在 RCTShadowView中的属性, 就会去布局 RCTShadowView 对应 UIView的全部子控件
10.[RCTView didUpdateReactSubviews] 给原生 View 添加子控件 完成 UI 渲染
render 函数的翻译
ReactElement.createElement = function (type, config, children){ ... }
UIManager 经过调用 createView 方法建立原生的 UIView
UIManager 经过 dispatchViewManagerCommand 来实现把原生UI 的方法给 JS 响应
UIManager 是一个 native moudle
这个模块是NativeModule方式定义的,在RN的JS端启动时,端上会经过JSC把收集到的模块信息(名称)打包到JS端全局变量global.__fbBatchedBridgeConfig中,并采用延迟加载策略:设置NativeModules.[模块名]的getter,延迟经过JSC读取模块详细信息(方法、命令号等信息)。在调用的时候会放到MessageQueue的队列里,批量提交,两次批量提交限制的最小间隔为5ms。
RN和原生同样,也是先渲染内部子控件,而后再渲染外部控件。因此Component来自React的,可是UI控件是React-Native的,在render生命周期执行的时候会执行子控件的render方法,子控件会调用UIManager来把信息传递到原始的UIManagerModule,UIManagerModule根据传过来的Tag找到对应的UIManager,最后生成一个Operation添加到UI处理队列中,当mDispatchUIRunnables执行runable的时候调用Operation.execute抽象方法,其实就是调用UIManager.createViewInstance来真正生成View,而后调用viewManager.updateProperties 设置View的属性。这样一个控件就建立出来了
// react- native 的事件处理流程
1.在建立RCTRootContentView的时候,内部会建立RCTTouchHandler
RCTTouchHandler:继承UIGestureRecognizer,也就是它就是一个手势
它会做为RCTRootContentView的手势,这样点击RCTRootContentView,就会触发RCTTouchHandler
RCTTouchHandler:内部实现了touchBegin等触摸方法,用来处理触摸事件
2.在建立RCTTouchHandler的时候,内部会建立RCTEventDispatcher
RCTEventDispatcher:用来把事件处理传递给JS的方法处理,也就是当UI界面产生事件,就会执行JS的代码处理。
3.经过RCTRootContentView截获点击事件
产生事件就会去触犯RCTRootContentView中的RCTTouchHandler对象。
4.当产生事件的时候,会执行[RCTTouchHandler touchBegin]
5.RCTTouchHandler的touch方法,会执行[RCTTouchHandler _updateAndDispatchTouches:eventName:]
内部会建立RCTTouchEvent事件对象
6.[RCTEventDispatcher sendEvent:event] -> 让事件分发对象调用发送事件对象
内部会把事件保存到_eventQueue(事件队列中)
7.[RCTEventDispatcher flushEventsQueue] -> 让事件分发对象冲刷事件队列,就是获取事件队列中全部事件执行
8.[RCTEventDispatcher dispatchEvent:event] -> 遍历事件队列,一个一个分发事件
分发事件的本质:就是去执行JS的代码,相应事件。
9.[RCTBatchedBridge enqueueJSCall:[[event class] moduleDotMethod] args:[event arguments]]; -> 让桥架对象调用JS处理事件
本质:就是产生事件调用JS代码
10.这样就能完成把UI事件交给JS代码响应
RN与原生(iOS)之间的通讯
OC端与 JS 端分别有一个Bridge, 两个 Bridge 都保存了一样一份模块配置表, OC 要告诉 JS 本身有什么模块 , 模块里有什么方法, JS 知道这些方法才能够调用, JS 调用 OC 模块方法的时候, 经过 Bridge 里的配置表把模块ID 方法 ID 和参数传给 OC, OC 经过配置表找到对应的方法执行, 如图
详细流程:
1.JS端调用某个OC模块暴露出来的方法。
2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。
在初始化时模块配置表上的每个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里能够拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实如今BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码,感觉下JS的魔力吧。
3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID表明callback。在经过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。
4.把上述步骤获得的ModuleID,MethodId,CallbackID和其余参数argus传给OC。至于具体是怎么传的,后面再说。
5.OC接收到消息,经过模块配置表拿到对应的模块和方法。
实际上模块配置表已经通过处理了,跟JS同样,在初始化时OC也对模块配置表上的每个模块生成了对应的实例并缓存起来,模块上的每个方法也都生成了对应的RCTModuleMethod对象,这里经过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实如今_handleRequestNumber:moduleID:methodID:params:。
6.RCTModuleMethod对JS传过来的每个参数进行处理。
RCTModuleMethod能够拿到OC要调用的目标方法的每一个参数类型,处理JS类型到目标类型的转换,全部JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。
例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。
这些参数组装完毕后,经过NSInvocation动态调用相应的OC模块方法。
7.OC模块方法调用完,执行block回调。
8.调用到第6步说明的RCTModuleMethod生成的block。
9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
10.MessageQueue经过CallbackID找到相应的JS callback方法。
11.调用callback方法,并把OC带过来的参数一块儿传过去,完成回调。
整个流程就是这样,简单归纳下,差很少就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS经过CallbackID拿到callback执行
说完原理,如今就是具体js怎么调用 OC , OC 怎么调用 JS
一 .JS调用 OC
1> 新建两个OC 文件遵照 RCTBridgeMoudle协议,这里以集成 native 的微博分享给 RN 调用为例
2> 经过 RCT_EXPORT_MOUDLE()暴露当前模块 RCT_EXPORT_METHOD()暴露 native 方法
3> 在 JS端经过 NativeModules获取当前 native 模块,调用模块的方法,传递参数,同时支持回调,在native方法里添加 block 回调或者 promise 回调,便可支持OC 回调传递结果给 JS
4> 导出常量原生模块能够导出一些常量,在JS 端能够随时访问,用这种方法来传递一些静态数据,能够避免经过 Bridge 进行一次来回交互
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
Js端访问 console.log(CalendarManager.firstDayOfTheWeek);
RCT_REMAP_METHOD(testPromiseEvent,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events =@[@"Promise ",@"test ",@" array"];
if (events) {
resolve(events);
} else {
NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
reject(@"no_events", @"There were no events", error);
}
}
// 固然也能够用这种 promise 的回调 在 js里可使用 . then() 或者 async await 来获取 promise 结果
//tips: 关于 RCT_EXPORT_METHOD() 能够暴露方法给 JS 调用, 内部实现就是利用 runtime 遍历该实例的方法, 过滤含有__ rct _export__的而后保存到模块配置表中
二. OC 调用 JS(给 JS 发送事件)
即便没有被js调用 本地模块也能够主动给 JS 发送事件通知, 最直接的方式是使用 eventDispatcher
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation CalendarManager
@synthesize bridge = _bridge;
// 进行设置发送事件通知给JavaScript端
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *name = [notification userInfo][@"name"];
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
body:@{@"name": name}];
}
@end
在 js中能够这样订阅事件import { NativeAppEventEmitter } from 'react-native';
var subscription = NativeAppEventEmitter.addListener(
'EventReminder',
(reminder) => console.log(reminder.name)
);
...
// 千万不要忘记忘记取消订阅, 一般在componentWillUnmount函数中实现。
subscription.remove();
三. RN 使用原生 UI 组件
1> 像原生自定义 UI 组件同样, 新建 class 自定义视图 , 这里以一个能够支持手势缩放的相册浏览视图为例
2> 新建 xxxViewManager 继承自 RCTViewManager
3> RCT_EXPORT_MODULE(RCTPhotoView)导出模块 RCT_EXPORT_VIEW_PROPERTY(imgURL, NSString); RCT_EXPORT_VIEW_PROPERTY(onSingleTap, RCTBubblingEventBlock);导出属性和方法供 js 调用
4> 重写 -(UIView *)View{ }的方法 返回自定义的UI 组件
5> 新建一个 JS 类 利用var RCTPhotoView = requireNativeComponent('RCTPhotoView', ImageBrowserView)
导出该自定义组件
6> 在须要的地方使用该组件(使用姿式与其余组件同样)
// ViewManager 的定义 .h
// 新建 JS 类导出该原生 UI 组件 使用的时候直接导入便可
// 使用 赋值 url属性 实现原生组件的点击方法的回调
// 最后说下 RN 的线程管理
RN 最主要的有兩個線程, UI MainThread 和 JSThread, UIThread 建立一个事件循环以后, 就一直有个 runloop 维持, 不断接收处理 App 事件, JSThread更像一个底层数据采集器, 不断上传数据和事件, ios 经过 JavascriptCore 提供的 js Bridge, UIThread将这些事件和数据转化为 UI 改变, UI main thread 跟 JS thread更像是CS 模型,JS thread更像服务端, UI main thread是客户端, UI main thread 不断询问JS thread而且请求数据,若是数据有变,则更新UI界面。
//RCTJavaScriptContext javascriptCore 引擎初始化
js 自己是单线程语言, native 是多线程机制, 那么 js如何来使用 native 的多线程, 或者说如何让js在其余线程执行
在 iOS 开发中,一谈到线程管理,确定离不开 GCD(Grand Central Dispatch)与 NSOperation/NSOperationQueue 技术选型上的争论。关于这二者广泛的观点为:GCD 较轻量,使用起来较灵活,但在任务依赖、并发数控制、任务状态控制(线程取消/挂起/恢复/监控)等方面存在先天不足;NSOperation/NSOperationQueue 基于 GCD 作的封装,使用较重,在某些情景下性能不如 GCD,但在并发环境下复杂任务处理能很好地知足一些特性,业务扩展性较好。
global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把须要调用的module,method,params都塞到队列里,而后传递到Native
__nativeCall(module, method, params, onFail, onSucc) {
this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); this._queue[PARAMS].push(params); global.nativeFlushQueueImmediate(this._queue); this._queue = [[],[],[]]; this._lastFlush = now;
}
Native模块查询接口:global.nativeRequireModuleConfig和调用接口global.nativeFlushQueueImmediate,他们是在JS引擎(JSContext)初始化时,定义到全局变量的。
先从 JS 端看起,JS 调用 Native 的逻辑在 MessageQueue.js 的 _nativeCall 方法中。在最小调用间隔(MIN_TIME_BETWEEN_FLUSHES_MS=5ms)内,JS 端会将调用信息存储在 _queue 数组中,经过 global. nativeFlushQueueImmediate 方法来调用 Native 端的功能。global.nativeFlushQueueImmediate 方法在 iOS 端映射的是一个全局的 Block,如图
nativeFlushQueueImmediate 在这里只是作了一个中转,功能的实现是经过调用 RCTBatchedBridge.m 中的 handleBuffer 方法。在 handleBuffer 中针对每一个组件使用一个 queue 来处理对应任务。其中,这个 queue 是组件数据 RCTModuleData 中的属性 methodQueue,后文会详细介绍。
虽然 JS 只具有单线程操做的能力,但经过利用 Native 端多线程处理能力,仍能够很好地处理 RN 中的任务。回到刚开始抛出的问题,RN 在这里用 GCD 而非 NSOperationQueue 来处理线程,主要缘由有:
// 自定义UI 组件的线程管理
Native(iOS)端处理并发任务的线程是 RCTModuleData 中的属性 methodQueue。RCTModuleData 是对组件对象的实例(instance)、方法(methods)、所属线程(methodQueue)等方面的描述。每个 module 都有个独立的线程来管理,具体线程的初始化在 RCTModuleData 的 setUpMethodQueue 中进行设置
这个方法开放了给组件自定义线程的接口。若是组件实现了 methodQueue 方法,则获取此方法中设置的 queue;不然默认建立一个子线程
是否是发现除了默认的 React-native 线程和主线程还有一个 RCTJSThread ,这是个什么东西, 看图:
说的很清楚了吧, 他不是一个线程,而是一个队列, 可以使 method 强制回到 js线程执行,
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}这样写 回到 js线程
// RCTUIManagerQueue
RN 中的 UI 操做都是在 RCTUIManagerQueue 中进行的, 他是一个并发队列,可是优先级是最高的, 因为苹果在 iOS 8.0 以后引入了 NSQualityOfService,淡化了原有的线程优先级概念,因此 RN 在这里优先使用了 8.0 的新功能,而对 8.0 如下的沿用原有的方式。但不论用哪一种方式,都保证 RCTUIManagerQueue 在并发队列中优先级是最高的。到这里或许有疑问了,UI 操做不是要在主线程里操做吗,这里为何是在一个子线程中操做?其实在此执行的是 UI 的准备工做,当真正须要把 UI 元素加入主界面,开始图形绘制时,才须要在主线程里操做
在這裡回到主線程操做
End