前言

React Native与传统的HybirdApp最大区别就是抛开WebView,使用JSC+原生组件的方式进行渲染,那么整个App启动/渲染流程又是怎样的呢?

React Native启动流程

首先从组件的角度来看下RN的启动流程:(Android为例)

  1. Native初始化,主要流程:ReactNativeHost -> Activity -> ReactRootView -> startReactApplication -> createReactContextInBackground(期间有模块/UI组件信息收集、JSC初始化等工作)
  2. 后台异步加载、执行JSBundle
  3. Native端执行setupReactContext初始化React上下文,调用JS端AppRegistry.runApplication(key,params),key为模块/组件名称,参数包含rootTag、initialProps
  4. JS端找到注册的对应启动组件,执行renderApplication渲染整个应用

renderApplication函数中会执行:

ReactNative.render(
<AppContainer>
<RootComponent
{...initialProps}
rootTag={rootTag}
/>
</AppContainer>,
rootTag
);

其中ReactNative是在React库中定义的,AppContainer是一个JS组件,使用View包裹了根组件,开发时工具InspectorYellowBox都是在这个组件中加载,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使用,可在此更改组件state
  • shouldComponentUpdate

    判断是否需要更新组件(在首次渲染期间或者调用了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的时候调用ReactNativeBaseComponentreceiveComponent -> 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)

    创建View
  • updateView(int tag, String className, ReadableMap props)

    更新View
  • manageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)

    批量添加/删除/移动一个view下面的view
  • measure(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 渲染实现分析的更多相关文章

  1. React Native 从入门到原理

    React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却寥寥无几. 本文分为两个部分:上半部分用通 ...

  2. 关于React Native 火热的话题,从入门到原理

    本文授权转载,作者:bestswifter(简书) React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原 ...

  3. React Native 从入门到原理一

    React Native 从入门到原理一 React Native 是最近非常火的一个话题,介绍如何利用 React Native 进行开发的文章和书籍多如牛毛,但面向入门水平并介绍它工作原理的文章却 ...

  4. React Native 入门到原理(详解)

    抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...

  5. 《React Native 精解与实战》书籍连载「React Native 底层原理」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  6. 腾讯优测优分享 | 探索react native首屏渲染最佳实践

    腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...

  7. 探索react native首屏渲染最佳实践

    文 / 腾讯 龚麒 0.前言 react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react n ...

  8. H5、React Native、Native应用对比分析

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博!iOS开发者交流QQ群: 446310206 "存在即合理".凡是存在的,都是合乎规律的.任何新 ...

  9. Hybrid APP基础篇(二)->Native、Hybrid、React Native、Web App方案的分析比较

    说明 Native.Hybrid.React.Web App方案的分析比较 目录 前言 参考来源 前置技术要求 楔子 几种APP开发模式 概述 Native App Web App Hybrid Ap ...

随机推荐

  1. 【.net 深呼吸】细说CodeDom(5):类型成员

    前文中,老周已经厚着脸皮介绍了类型的声明,类型里面包含的自然就是类型成员了,故,顺着这个思路,今天咱们就了解一下如何向类型添加成员. 咱们都知道,常见的类型成员,比如字段.属性.方法.事件.表示代码成 ...

  2. 【声明】前方不设坑位,不收费!~ 我为NET狂官方学习计划

    发个通知,过段时间学习计划相关的东西就出来了,上次写了篇指引文章后有些好奇心颇重的人跟我说:“发现最近群知识库和技能库更新的频率有点大,这是要放大招的节奏啊!” 很多想学习却不知道如何规划的人想要一个 ...

  3. HTML DOM 对象

    本篇主要介绍HTML DOM 对象:Document.Element.Attr.Event等4个对象. 目录 1. Document 对象:表示文档树的根节点,大部分属性和方法都是对元素进行操作. 2 ...

  4. spark处理大规模语料库统计词汇

    最近迷上了spark,写一个专门处理语料库生成词库的项目拿来练练手, github地址:https://github.com/LiuRoy/spark_splitter.代码实现参考wordmaker ...

  5. 【Machine Learning】Python开发工具:Anaconda+Sublime

    Python开发工具:Anaconda+Sublime 作者:白宁超 2016年12月23日21:24:51 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现 ...

  6. Redis简单案例(二) 网站最近的访问用户

    我们有时会在网站中看到最后的访问用户.最近的活跃用户等等诸如此类的一些信息.本文就以最后的访问用户为例, 用Redis来实现这个小功能.在这之前,我们可以先简单了解一下在oracle.sqlserve ...

  7. python 数据类型 --- 集合

    1. 注意列表和集合的区别 set 列表表现形式: list_1 = [1,3,4];  集合表现形式:set_1= set() list_1 = [1,2,3,4,23,4,2] print(lis ...

  8. Maven 代理设置

    在maven的安装目录下 %MAVEN_HOME%/conf/setting.xml 中进行设置 <proxies>    <!-- proxy     | Specificatio ...

  9. 【夯实PHP基础】nginx php-fpm 输出php错误日志

    本文地址 原文地址 分享提纲: 1.概述 2.解决办法(解决nginx下php-fpm不记录php错误日志) 1. 概述 nginx是一个web服务器,因此nginx的access日志只有对访问页面的 ...

  10. iOS:以前笔记,未整理版。太多了,先放着吧。。。。。。。

    1. -(void)timetick { _d = 0; NSTimer *newtime =[NSTimer scheduledTimerWithTimeInterval:1 target:self ...