React-Native 渲染实现分析
前言
React Native与传统的HybirdApp最大区别就是抛开WebView,使用JSC+原生组件的方式进行渲染,那么整个App启动/渲染流程又是怎样的呢?
React Native启动流程
首先从组件的角度来看下RN的启动流程:(Android为例)
- Native初始化,主要流程:ReactNativeHost -> Activity -> ReactRootView -> startReactApplication -> createReactContextInBackground(期间有模块/UI组件信息收集、JSC初始化等工作)
- 后台异步加载、执行JSBundle
- Native端执行
setupReactContext
初始化React上下文,调用JS端AppRegistry.runApplication(key,params)
,key为模块/组件名称,参数包含rootTag、initialProps - JS端找到注册的对应启动组件,执行
renderApplication
渲染整个应用
renderApplication
函数中会执行:
ReactNative.render(
<AppContainer>
<RootComponent
{...initialProps}
rootTag={rootTag}
/>
</AppContainer>,
rootTag
);
其中ReactNative
是在React库中定义的,AppContainer
是一个JS组件,使用View包裹了根组件,开发时工具Inspector
、YellowBox
都是在这个组件中加载,RootComponent
是传入的根组件。
JS端注册组件:(在第2步执行JSBundle时)
AppRegistry.registerComponent('TiebaNext', rootComponent);
*仅在JS端处理,记录在一个Map中。
Android端定义启动组件,Activity中,继承ReactActivity:(在第1步时调用)
@Override
protected String getMainComponentName() {
return "TiebaNext";
}
iOS端定义启动组件:
self.rctRootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"TiebaNext"
initialProperties:nil
launchOptions:nil];
简单说就是Native初始化 -> 加载JS,JS端注册组件 -> 端上调用JS端run方法,传入入口组件名称 -> JS端启动渲染流程。
React Native渲染流程
React的渲染都是以组件为单位,上面已经分析了,启动的最后阶段就是JS端开始渲染根组件。首先我们先看下React的组件是怎么编写的,以及他的生命周期:(熟悉React可略过)
一个例子,无网络提示组件:
(例子语言Typescript)
// 组件的属性定义
interface PropsDefine {
// 组件宽度
width: number
// 组件高度
height: number
// 点击刷新按钮回调,可选
onClickRefresh?: () => void
}
export class NoNetwork extends React.Component<PropsDefine, {}> { // 组件无状态,定义为空:{}
// 组件的默认属性定义,单例,实例间共享
static defaultProps = {
onClickRefresh: () => { }
}
render() {
let {width, height} = this.props
return (
<View style={[Styles.panel, {
width: width,
height: height,
}]}>
<View style={Styles.picBlock}>
<Image source={Styles.picUrl}/>
</View>
<View style={Styles.textBlock}>
<Text style={Styles.text}>你的网络好像不给力</Text>
<Text style={Styles.text}>点击按钮刷新</Text>
</View>
<TouchableOpacity style={Styles.button} onPress={this.props.onClickRefresh}>
<Text style={Styles.buttonText}>刷新</Text>
</TouchableOpacity>
</View>
)
}
}
跟端上组件开发一样,React组件也定义了组件的生命周期:
实例化
getDefaultProps
组件类型首次实例化时初始化默认props属性,多实例共享getInitialState
实例化时初始化默认state属性componentWillMount
在渲染之前触发一次render
渲染函数,返回DOM结构componentDidMount
在渲染之后触发一次
有需要重新渲染(props变更或者setState改变state时)
componentWillReceiveProps
组件接收到新的props时调用,并将其作为参数nextProps使用,可在此更改组件stateshouldComponentUpdate
判断是否需要更新组件(在首次渲染期间或者调用了forceUpdate方法后,该方法不会被调用)componentWillUpdate
更新渲染前调用render
渲染函数,返回DOM结构componentDidUpdate
更新渲染后调用
销毁
componentWillUnmount
组件移除之前调用
那么这个组件到底是怎么用原生组件渲染的呢?首先我们先来看看最主要的render做了什么。jsx不太直观,我们先翻译一下render:
render() {
let { width, height } = this.props;
return (React.createElement(View, { style: [Styles.panel, {
width: width,
height: height,
}] },
React.createElement(View, { style: Styles.picBlock },
React.createElement(Image, { source: Styles.picUrl })),
React.createElement(View, { style: Styles.textBlock },
React.createElement(Text, { style: Styles.text }, "\u4F60\u7684\u7F51\u7EDC\u597D\u50CF\u4E0D\u7ED9\u529B"),
React.createElement(Text, { style: Styles.text }, "\u70B9\u51FB\u6309\u94AE\u5237\u65B0")),
React.createElement(TouchableOpacity, { style: Styles.button, onPress: this.props.onClickRefresh },
React.createElement(Text, { style: Styles.buttonText }, "\u5237\u65B0"))));
}
这下清晰多了吧?
React.createElement
的方法签名:
ReactElement.createElement = function (type, config, children){ ... }
ReactNative的UI组件通过requireNativeComponent
-> createReactNativeComponentClass
-> ReactNativeBaseComponent下mountComponent
的调用关系,最终在mountComponent
中调用UIManager
组件创建View:UIManager.createView(tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload);
,在Native端,UIManager调用对应组件类型的ViewManager(单例,管理类)创建实例。
*UIManager
是一个NativeModule,待下面分析
接下来我们来详细分析下原生组件的实现方法,以Image组件为例:
iOS和Android实现有一定差异,首先是Image组件JS端代码,都需要requireNativeComponent
加载原生组件:
const RCTImageView = requireNativeComponent('RCTImageView', Image);
Image的JS端实际上也是一个React JS组件,他也有render,返回的是:(iOS)
<RCTImageView
{...this.props}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={sources}
/>
因为业务逻辑是写在JS端的,创建出了Native组件就需要进行控制,自然就涉及到属性传递、方法调用、事件回调这3个需求。
Native组件跟JS端通讯方式
JS端组件跟Native真正实现的组件主要涉及三件事:
- 属性同步
- JS端调用Native方法
- Native事件回调JS端
属性同步
属性同步很简单,实际上是在组件重新render的时候调用ReactNativeBaseComponent
下receiveComponent
-> UIManager.updateView
完成的。
JS端调用Native方法
两种方法,一种是调用NativeModules
(后面有简单分析),如果想直接调用一个具体View的方法,那就需要使用UIManager模块:
Android端UIManager中的定义:
@ReactMethod
public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
}
iOS端UIManager中的定义:
RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
commandID:(NSInteger)commandID
commandArgs:(NSArray<id> *)commandArgs)
{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
Class managerClass = componentData.managerClass;
RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
id<RCTBridgeMethod> method = moduleData.methods[commandID];
NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
[method invokeWithBridge:_bridge module:componentData.manager arguments:args];
}
这个方法是从端上映射到JS的,所以在JS端可以这样调用:
UIManager.dispatchViewManagerCommand(
findNodeHandle(this), // 找到与NativeUI组件对应的JS组件实例
UIManager.[UI组件名].Commands.[方法],
[] // 参数
)
findNodeHandle
方法是在React中定义,可以找到组件实例的reactTag
(执行在JS端),UIManager可以把调用命令分发到Native端对应的组件类型的ViewManager,再通过ViewManager调用View组件实例的对应方法。
Native事件回调JS端
Android端使用的是类似JS端调用Native的方式,使用了事件机制,不过事件的接收者是从JS端映射过来的,React下ReactNativeEventEmitter.receiveEvent(tag, topLevelType, nativeEventParam)
,所以需要先实现一个Event:(Switch的onValueChange事件)
class ReactSwitchEvent extends Event<ReactSwitchEvent> {
public static final String EVENT_NAME = "topChange"; // topChange会被映射成onChange,具体映射关系参见 UIManagerModuleConstants.java
public ReactSwitchEvent(int viewId, boolean isChecked) {
super(viewId);
mIsChecked = isChecked;
}
public boolean getIsChecked() {
return mIsChecked;
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public short getCoalescingKey() {
// All switch events for a given view can be coalesced.
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
}
private WritableMap serializeEventData() {
WritableMap eventData = Arguments.createMap();
eventData.putInt("target", getViewTag());
eventData.putBoolean("value", getIsChecked());
return eventData;
}
}
然后在ViewManager或View中进行事件派发:
ReactContext reactContext = (ReactContext) buttonView.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
new ReactSwitchEvent(
buttonView.getId(),
isChecked));
iOS端实现有所区别,iOS端将JS函数直接映射到Native,所以可以直接调用(可多次调用):(View为RCTSwitch)
// ViewManager中声明事件为RCTBubblingEventBlock或RCTDirectEventBlock
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);
// View中声明
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
// view实例化时监听onChange
- (void)onChange:(RCTSwitch *)sender
{
if (sender.wasOn != sender.on) {
if (sender.onChange) {
sender.onChange(@{ @"value": @(sender.on) });
}
sender.wasOn = sender.on;
}
}
这样就可以从JS端创建NativeUI组件了,可以看到UI组件的Native和JS端是通过reactTag进行的关联,通过UIManager模块,在Native端的DOM和React的DOM进行同步操作,保持结构一致。
UIManager
模块数据结构,JS端可访问:
UIManager.[UI组件名].[Constants(静态值)/Commands(命令/方法)]
从端上映射的方法:(部分)
createView(int tag, String className, int rootViewTag, ReadableMap props)
创建ViewupdateView(int tag, String className, ReadableMap props)
更新ViewmanageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)
批量添加/删除/移动一个view下面的viewmeasure(int reactTag, Callback callback)
测量View的位置、size等,结果异步回调measureInWindow(int reactTag, Callback callback)
测量View相对屏幕的位置、size等,结果异步回调dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs)
派发View命令,也就是用来调用对应View的方法
这个模块是NativeModule方式定义的,在RN的JS端启动时,端上会通过JSC把收集到的模块信息(名称)打到JS端全局变量global.__fbBatchedBridgeConfig
中,并采用延迟加载策略:设置NativeModules.[模块名]
的getter,延迟通过JSC读取模块详细信息(方法、命令号等信息)。在调用的时候会放到MessageQueue
的队列里,批量提交,两次批量提交限制的最小间隔为5ms。
关于React Native通讯更详尽的分析参见:React Native通讯原理
React-Native 渲染实现分析的更多相关文章
- React Native 从入门到原理
React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却寥寥无几. 本文分为两个部分:上半部分用通 ...
- 关于React Native 火热的话题,从入门到原理
本文授权转载,作者:bestswifter(简书) React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原 ...
- React Native 从入门到原理一
React Native 从入门到原理一 React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却 ...
- React Native 入门到原理(详解)
抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...
- 《React Native 精解与实战》书籍连载「React Native 底层原理」
此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...
- 腾讯优测优分享 | 探索react native首屏渲染最佳实践
腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...
- 探索react native首屏渲染最佳实践
文 / 腾讯 龚麒 0.前言 react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react n ...
- H5、React Native、Native应用对比分析
每日更新关注:http://weibo.com/hanjunqiang 新浪微博!iOS开发者交流QQ群: 446310206 "存在即合理".凡是存在的,都是合乎规律的.任何新 ...
- Hybrid APP基础篇(二)->Native、Hybrid、React Native、Web App方案的分析比较
说明 Native.Hybrid.React.Web App方案的分析比较 目录 前言 参考来源 前置技术要求 楔子 几种APP开发模式 概述 Native App Web App Hybrid Ap ...
随机推荐
- 【.net 深呼吸】细说CodeDom(5):类型成员
前文中,老周已经厚着脸皮介绍了类型的声明,类型里面包含的自然就是类型成员了,故,顺着这个思路,今天咱们就了解一下如何向类型添加成员. 咱们都知道,常见的类型成员,比如字段.属性.方法.事件.表示代码成 ...
- 【声明】前方不设坑位,不收费!~ 我为NET狂官方学习计划
发个通知,过段时间学习计划相关的东西就出来了,上次写了篇指引文章后有些好奇心颇重的人跟我说:“发现最近群知识库和技能库更新的频率有点大,这是要放大招的节奏啊!” 很多想学习却不知道如何规划的人想要一个 ...
- HTML DOM 对象
本篇主要介绍HTML DOM 对象:Document.Element.Attr.Event等4个对象. 目录 1. Document 对象:表示文档树的根节点,大部分属性和方法都是对元素进行操作. 2 ...
- spark处理大规模语料库统计词汇
最近迷上了spark,写一个专门处理语料库生成词库的项目拿来练练手, github地址:https://github.com/LiuRoy/spark_splitter.代码实现参考wordmaker ...
- 【Machine Learning】Python开发工具:Anaconda+Sublime
Python开发工具:Anaconda+Sublime 作者:白宁超 2016年12月23日21:24:51 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现 ...
- Redis简单案例(二) 网站最近的访问用户
我们有时会在网站中看到最后的访问用户.最近的活跃用户等等诸如此类的一些信息.本文就以最后的访问用户为例, 用Redis来实现这个小功能.在这之前,我们可以先简单了解一下在oracle.sqlserve ...
- python 数据类型 --- 集合
1. 注意列表和集合的区别 set 列表表现形式: list_1 = [1,3,4]; 集合表现形式:set_1= set() list_1 = [1,2,3,4,23,4,2] print(lis ...
- Maven 代理设置
在maven的安装目录下 %MAVEN_HOME%/conf/setting.xml 中进行设置 <proxies> <!-- proxy | Specificatio ...
- 【夯实PHP基础】nginx php-fpm 输出php错误日志
本文地址 原文地址 分享提纲: 1.概述 2.解决办法(解决nginx下php-fpm不记录php错误日志) 1. 概述 nginx是一个web服务器,因此nginx的access日志只有对访问页面的 ...
- iOS:以前笔记,未整理版。太多了,先放着吧。。。。。。。
1. -(void)timetick { _d = 0; NSTimer *newtime =[NSTimer scheduledTimerWithTimeInterval:1 target:self ...