前一篇,主要讲了 RN 如何初始化一个 BCTBatchedBridge
,这一篇将进一步研究 RN 是如何通过这个 bridge 创建出真正展示出来的 View 的。
首先抛出结论:每一个 js render()
方法中的 component 都对应于 native 中的一个 RCTView
。在渲染前,js 会循环取出各个 component 。然后通过 UIManager
(NativeModules
中的一项) 的 createView
方法,通过 BatchedBridge
调用 native 端的 RCTUIManager
的同名方法,创建与 component 相对应的类。对于触摸事件也是类似的。native 端通过 bridge 将触摸事件传给 js,js 在找到对应 component 后,响应各个触摸事件。
下面将分几个阶段解读 RN 是如何显示出来的,依旧是 0.39 版本。最好在理解了前一篇 RN 通信原理后再看。
native 调用 js
这是整个过程的第一步,从 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge moduleName:moduleName initialProperties:param];
方法开始。native 告诉 js 我要准备显示 view 了,快点把你这里各个 view 的信息告诉我。先来看一张调用流程图:
tag1
这个 setBridge
方法是初始化时候执行的方法。还记得前一篇初始化的时候其中的一步设置各个 RCTModuleData
的 instance
么?其中就会调用 setBridge
方法为每个 RCTModuleData 的 instance
设置 bridge。在 RCTUIManager
类的 setBridge
方法中还做了一些其他操作:
1 | - (void)setBridge:(RCTBridge *)bridge{ |
该方法循环遍历 bridge 中的每一个暴露给 js 的模块名,判断是否是 RCTViewManager
的子类。如果是,则为其创建一个 RCTComponentData
并将其保存在 _componentDataByName
字典中。RCTViewManager
是各个 RCTView
的控制器的父类,所有 RCTView
的方法都在 RCTViewManager
的子类里实现。这样就把所有视图控制器保存在了 RCTUIManager
中。
tag2
看到这个方法的名字就知道是在 bundle 加载完成后执行的。 这个方法分为两步。第一步是创建一个 _contentView
用来容纳各个 component。第二步就是通过 BatchedBridge
调用 js 的方法了。部分代码如下:
1 | - (void)bundleFinishedLoading:(RCTBridge *)bridge |
其中有一个地方需要说明,就是这里面的 self.reactTag
。类似于 RN 通信的原理,reactTag
是 native 和 js 识别各个 component 的标记。来看一下 reactTag
的 getter 方法:
1 | - (NSNumber *)reactTag |
每一个根视图都必须有一个独一无二的 ractTag
。这个 tag 从1开始,每次自增10,也就是 1,11,21,31…的形式,以此类推。
这里要吐槽一下 facebook 的程序员。没想到外国的程序员也会把单词拼错。上面方法中 sizeFlexiblity
少了个 i
。这是要输入法背锅么?
tag3
该方法首先为 rootView
新建了 RCTTouchHandler
实例,并设置为其 GestureRecognizer
。其次执行了 registerRootView:withSizeFlexibility:
方法。这个方法将 rootView
保存在 RCTUIManager
的 _viewRegistry
字典中,其中键是 rootView
的 reactTag
。此时,RCTUIManager
中设置并保存好了两个比较重要的字典:_viewRegistry
,_componentDataByName
。
tag4
这个方法就比较直观了:
1 | - (void)runApplication:(RCTBridge *)bridge |
就是调用 enqueueJSCall:method:args:completion:
方法。如果前一篇看懂了的话,就会知道,这个方法调用的是 js 端 AppRegistry.js
的 runApplication
方法,需要创建的 rootView
以 String 的方式通过 moduleName
传入。
到这一步,Native 调用 JS 就结束了,接下来就是 js 端的执行。
js 调用 native
js 端的方法调用过程还是比较清晰的。来看一下下面的流程图:
tag1
在 native 调用 enqueueJSCall:method:args:completion
后,我们来看看 AppRegistry
的 runApplication
方法。
1 | runApplication: function(appKey: string, appParameters: any): void { |
appKey
就是上面的 moduleName
,通过它拿到 runnables
字典中对应的对象,执行其 run
方法。这个字典怎么来的呢?往下看。
tag2
RN 的官方文档中第一篇就说明了,在定义好一个 component 后,需要在 index.ios.js
中使用 AppRegistry.registerComponent()
方法注册:
1 | import HelloWorldView from './app/view/HelloWorldView'; |
那么 registerComponent
做了什么呢?
1 | registerComponent: function(appKey: string, getComponentFunc: ComponentProvider): string { |
这里就设置了前面用到的 runnables
字典,并且为字典内对象创建了 run
方法。前面执行的 run
方法,其实就是执行 renderApplication
方法。renderApplication
在 RootView 外层又嵌套了一层 AppContainer
。
tag3
这个方法内部就比较复杂了。其中:
1 | var instance = instantiateReactComponent(nextWrappedElement, false); |
通过 instantiateReactComponent
方法将整个 View 的 JSX 转换为 ReactCompositeComponentWrapper
实例。这也就是 Reactjs 的精髓之一。好吧。并没有花时间去细读,臣妾做不到啊。有兴趣的话可以找找类似于 reactjs源码分析-上篇(首次渲染实现原理) 的文章学习一下。之后就是长长的调用栈了,循环递归每一个 component 执行 createView
。
tag4
在 ReactNativeBaseComponent.js
中调用了 UIManager.createView()
,但是这个方法点不进去,UIManager.js
中没有这个方法。开始我比较迷惑,后来细读了一下发现:
1 | const { UIManager } = NativeModules; |
也就是说,这个 UIManager
是 native 中的一个 module,这个 createView()
是 native 中暴露出来的一个方法。
1 | UIManager.createView( |
这个方法会将当前 component 的标记,名字,根标记,属性方法等一起传给 native。其中 tag
是 ReactNativeTagHandles.allocateTag()
生成的。每次调用该方法,tag
都会加一(会略过1结尾的,那些是留给根 component 的)。
至此,js 调用 native 的部分就结束了。回顾一下,主要就是循环每一个 component,为每个 component 分配一个独有的 tag
然后调用 createView
方法。
Native createView 的过程
来到 native,RCTUIManager
暴露出了许多方法供 js 调用。来看一下刚才 js 调用的 createView
方法:
1 | RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag |
_componentDataByName
上面介绍过了,可以拿到前面保存的 js 将要创建的 view 的 RCTComponentData
。然后在主线程中使用 createViewWithTag:
方法创建 View。通过 RCTComponentData
中保存的 _managerClass
可以拿到对应 View 的 manager
,然后调用其 view
方法创建 view。
1 | - (UIView *)createViewWithTag:(NSNumber *)tag |
通过 setProps:forView:
方法将每个 view 的 props 保存下来。这里又要用到反射的方式,还是比较复杂的。具体就不说了。只要清楚,它会生成一个 block,每次都用这个 block,都会调用对应属性的 setter 方法将值赋给该属性即可。
最后,把设置好的 view 保存到 _viewRegistry
中。js 和 native 端都会保存好这个 tag
和 view 的映射表。这里的 _viewRegistry
就是 native 端保存的字典。
这里只是介绍了如何通过 js 传来的 tag
和 moduleName
创建出对应的 native 端的 view 的。当然,创建和展示中间还隔着 measure、layout之类的步骤,这也是 React 需要做的,这个我就不聊了,太深入了聊不动,容易聊爆。
自建组件
现在我们知道 view 是如何创建出来的了。facebook 封装的组件和自建的组件原理上是一致的。现在来研究一下自建组件的创建过程。
基本设置
前面说到拿到 view 的 manager
调用其 view
方法。那么 manager
是个什么呢?manager
相当于一个 Controller,负责创建和控制 view。文档上关于创建 RCTViewManager
主要有三步:
- 创建一个子类
- 添加
RCT_EXPORT_MODULE()
标记宏 - 实现
-(UIView *)view
方法
这三步是最基本的三步,作用也挺明显的。继承 RCTViewManager
作为一个标记,这样 RCTUIManager
在设置 bridge
的时候就能够将该 manager
放入 _componentDataByName
数组中了。添加 RCT_EXPORT_MODULE()
标记宏后才能将该类暴露给 js,才能让 js 调用该类方法。实现 -(UIView *)view
方法就是上面 [self.manager view]
调用的方法。
设置属性
上面只是创建了一个 view,更复杂一点的情况就是需要控制 view 的属性。RN 提供了两个宏:
RCT_EXPORT_VIEW_PROPERTY(name,type)
RCT_CUSTOM_VIEW_PROPERTY(name,type,viewClass){...}
来看一下宏定义的实现:
1 | // 常规导出 |
关于 ##
和 @#
前一篇也有涉及。操作符 ##
用来实现宏中 token 的连接, @#
实现将宏中的参数转化为字符串。比如 RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
这样一个属性,将会被转化为下面这个方法:
1 | + (NSArray<NSString *> *)propConfig_placeholder { return @[@"NSString"]; } |
但如果要对属性进行也谢操作的话,比如:
1 | RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RCTTextView) |
这样的自定义属性,则会转化为如下方法:
1 | + (NSArray<NSString *> *)propConfig_fontSize { return @[@"NSNumber", @"__custom__"]; } |
其中 json
代表了 JS 中传递的尚未解析的原始值。view
代表访问的对应的视图实例。defaultView
在 JS 发送 null 的时候,可以把视图的这个属性重置回默认值。
这里说一下这个 defaultView
,这是 RCTComponentData
中的一个属性,不用在意也不用自己设置它。在 json
为空的时候,就会去自己创建一个当前 view 的实例作为 defaultView
:
1 | //RCTComponentData.m propBlockForKey:inDictionary |
宏定义的方法在上面说过的 setProps:forView:
方法里调用,目的是验证从 js 端传递过来的属性是否在 native 里注册过。
至于还有一些非常规的需要用到 RCTConvert
以及事件的使用方式可以参考文档,不深入了。
创建 View
前面说过了如何创建 Controller,现在说一下这个 view。
1 | - (UIView *)view |
这是官方文档里的例子,在 manager
中的 view
方法里创建了一个 MKMapView
的视图。那么 RN 是如何设置 MKMapView
的 frame 的呢?不用担心,RN 实现了 UIView
的范畴 UIView(React)
,其中就实现了 reactSetFrame
方法,会自己将计算好的大小设置给 view,如果你想在 view 设置好大小后做什么事,你就可以通过重写这个方法。另外,当 js 端的一些属性变化时,就会触发 native 相应 view 的相应属性的 setter 方法,可以自己实现这些 setter 方法,来做一些别的处理,比如在 RCTImageView
里,只要任意属性发生变化,都会触发 reloadImage
方法。
终
总算通过这两篇将 RN 稍微梳理了一下,跟进起来不容易啊,都是泪。本来想研究一下,写一篇 “RN view 显示原理” 的。但是读着读着发现 Reactjs 并不能看懂,那还写个蛋的显示原理啊喂。最终就忽略了 view 的层次关系以及大小位置等等的转换原理,写了这么一篇。虽然写的水平不高,但是我相信如果你想更深入的了解一下 react native,读一下本篇还是能有些许帮助的。毕竟为了看懂,我也读了好久好久~~~
也许会有下一篇,也许会挑一个 RN 封装的组件具体看看它是如何实现的~