前言

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函数中会执行:

  1. ReactNative.render(
  2. <AppContainer>
  3. <RootComponent
  4. {...initialProps}
  5. rootTag={rootTag}
  6. />
  7. </AppContainer>,
  8. rootTag
  9. );

其中ReactNative是在React库中定义的,AppContainer是一个JS组件,使用View包裹了根组件,开发时工具InspectorYellowBox都是在这个组件中加载,RootComponent是传入的根组件。

JS端注册组件:(在第2步执行JSBundle时)

  1. AppRegistry.registerComponent('TiebaNext', rootComponent);

*仅在JS端处理,记录在一个Map中。

Android端定义启动组件,Activity中,继承ReactActivity:(在第1步时调用)

  1. @Override
  2. protected String getMainComponentName() {
  3. return "TiebaNext";
  4. }

iOS端定义启动组件:

  1. self.rctRootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
  2. moduleName:@"TiebaNext"
  3. initialProperties:nil
  4. launchOptions:nil];

简单说就是Native初始化 -> 加载JS,JS端注册组件 -> 端上调用JS端run方法,传入入口组件名称 -> JS端启动渲染流程。

React Native渲染流程

React的渲染都是以组件为单位,上面已经分析了,启动的最后阶段就是JS端开始渲染根组件。首先我们先看下React的组件是怎么编写的,以及他的生命周期:(熟悉React可略过)

一个例子,无网络提示组件:

(例子语言Typescript)

  1. // 组件的属性定义
  2. interface PropsDefine {
  3. // 组件宽度
  4. width: number
  5. // 组件高度
  6. height: number
  7. // 点击刷新按钮回调,可选
  8. onClickRefresh?: () => void
  9. }
  10. export class NoNetwork extends React.Component<PropsDefine, {}> { // 组件无状态,定义为空:{}
  11. // 组件的默认属性定义,单例,实例间共享
  12. static defaultProps = {
  13. onClickRefresh: () => { }
  14. }
  15. render() {
  16. let {width, height} = this.props
  17. return (
  18. <View style={[Styles.panel, {
  19. width: width,
  20. height: height,
  21. }]}>
  22. <View style={Styles.picBlock}>
  23. <Image source={Styles.picUrl}/>
  24. </View>
  25. <View style={Styles.textBlock}>
  26. <Text style={Styles.text}>你的网络好像不给力</Text>
  27. <Text style={Styles.text}>点击按钮刷新</Text>
  28. </View>
  29. <TouchableOpacity style={Styles.button} onPress={this.props.onClickRefresh}>
  30. <Text style={Styles.buttonText}>刷新</Text>
  31. </TouchableOpacity>
  32. </View>
  33. )
  34. }
  35. }

跟端上组件开发一样,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:

  1. render() {
  2. let { width, height } = this.props;
  3. return (React.createElement(View, { style: [Styles.panel, {
  4. width: width,
  5. height: height,
  6. }] },
  7. React.createElement(View, { style: Styles.picBlock },
  8. React.createElement(Image, { source: Styles.picUrl })),
  9. React.createElement(View, { style: Styles.textBlock },
  10. React.createElement(Text, { style: Styles.text }, "\u4F60\u7684\u7F51\u7EDC\u597D\u50CF\u4E0D\u7ED9\u529B"),
  11. React.createElement(Text, { style: Styles.text }, "\u70B9\u51FB\u6309\u94AE\u5237\u65B0")),
  12. React.createElement(TouchableOpacity, { style: Styles.button, onPress: this.props.onClickRefresh },
  13. React.createElement(Text, { style: Styles.buttonText }, "\u5237\u65B0"))));
  14. }

这下清晰多了吧?

React.createElement的方法签名:

  1. 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加载原生组件:

  1. const RCTImageView = requireNativeComponent('RCTImageView', Image);

Image的JS端实际上也是一个React JS组件,他也有render,返回的是:(iOS)

  1. <RCTImageView
  2. {...this.props}
  3. style={style}
  4. resizeMode={resizeMode}
  5. tintColor={tintColor}
  6. source={sources}
  7. />

因为业务逻辑是写在JS端的,创建出了Native组件就需要进行控制,自然就涉及到属性传递、方法调用、事件回调这3个需求。

Native组件跟JS端通讯方式

JS端组件跟Native真正实现的组件主要涉及三件事:

  • 属性同步
  • JS端调用Native方法
  • Native事件回调JS端

属性同步

属性同步很简单,实际上是在组件重新render的时候调用ReactNativeBaseComponentreceiveComponent -> UIManager.updateView完成的。

JS端调用Native方法

两种方法,一种是调用NativeModules(后面有简单分析),如果想直接调用一个具体View的方法,那就需要使用UIManager模块:

Android端UIManager中的定义:

  1. @ReactMethod
  2. public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
  3. mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
  4. }

iOS端UIManager中的定义:

  1. RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
  2. commandID:(NSInteger)commandID
  3. commandArgs:(NSArray<id> *)commandArgs)
  4. {
  5. RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
  6. RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
  7. Class managerClass = componentData.managerClass;
  8. RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
  9. id<RCTBridgeMethod> method = moduleData.methods[commandID];
  10. NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
  11. [method invokeWithBridge:_bridge module:componentData.manager arguments:args];
  12. }

这个方法是从端上映射到JS的,所以在JS端可以这样调用:

  1. UIManager.dispatchViewManagerCommand(
  2. findNodeHandle(this), // 找到与NativeUI组件对应的JS组件实例
  3. UIManager.[UI组件名].Commands.[方法],
  4. [] // 参数
  5. )

findNodeHandle方法是在React中定义,可以找到组件实例的reactTag(执行在JS端),UIManager可以把调用命令分发到Native端对应的组件类型的ViewManager,再通过ViewManager调用View组件实例的对应方法。

Native事件回调JS端

Android端使用的是类似JS端调用Native的方式,使用了事件机制,不过事件的接收者是从JS端映射过来的,React下ReactNativeEventEmitter.receiveEvent(tag, topLevelType, nativeEventParam),所以需要先实现一个Event:(Switch的onValueChange事件)

  1. class ReactSwitchEvent extends Event<ReactSwitchEvent> {
  2. public static final String EVENT_NAME = "topChange"; // topChange会被映射成onChange,具体映射关系参见 UIManagerModuleConstants.java
  3. public ReactSwitchEvent(int viewId, boolean isChecked) {
  4. super(viewId);
  5. mIsChecked = isChecked;
  6. }
  7. public boolean getIsChecked() {
  8. return mIsChecked;
  9. }
  10. @Override
  11. public String getEventName() {
  12. return EVENT_NAME;
  13. }
  14. @Override
  15. public short getCoalescingKey() {
  16. // All switch events for a given view can be coalesced.
  17. return 0;
  18. }
  19. @Override
  20. public void dispatch(RCTEventEmitter rctEventEmitter) {
  21. rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
  22. }
  23. private WritableMap serializeEventData() {
  24. WritableMap eventData = Arguments.createMap();
  25. eventData.putInt("target", getViewTag());
  26. eventData.putBoolean("value", getIsChecked());
  27. return eventData;
  28. }
  29. }

然后在ViewManager或View中进行事件派发:

  1. ReactContext reactContext = (ReactContext) buttonView.getContext();
  2. reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
  3. new ReactSwitchEvent(
  4. buttonView.getId(),
  5. isChecked));

iOS端实现有所区别,iOS端将JS函数直接映射到Native,所以可以直接调用(可多次调用):(View为RCTSwitch)

  1. // ViewManager中声明事件为RCTBubblingEventBlock或RCTDirectEventBlock
  2. RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);
  3. // View中声明
  4. @property (nonatomic, copy) RCTBubblingEventBlock onChange;
  5. // view实例化时监听onChange
  6. - (void)onChange:(RCTSwitch *)sender
  7. {
  8. if (sender.wasOn != sender.on) {
  9. if (sender.onChange) {
  10. sender.onChange(@{ @"value": @(sender.on) });
  11. }
  12. sender.wasOn = sender.on;
  13. }
  14. }

这样就可以从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. React 入门教程

    React 起源于Facebook内部项目,是一个用来构建用户界面的 javascript 库,相当于MVC架构中的V层框架,与市面上其他框架不同的是,React 把每一个组件当成了一个状态机,组件内 ...

  2. 我们是怎么做Code Review的

    前几天看了<Code Review 程序员的寄望与哀伤>,想到我们团队开展Code Review也有2年了,结果还算比较满意,有些经验应该可以和大家一起分享.探讨.我们为什么要推行Code ...

  3. 第一个shell脚本

    打开文本编辑器,新建一个文件,扩展名为sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好. #!/bin/bash echo "Hello World !" &quo ...

  4. centos7+mono4+jexus5.6.2安装过程中的遇到的问题

    过程参考: http://www.linuxdot.net/ http://www.jexus.org/ http://www.mono-project.com/docs/getting-starte ...

  5. 结巴分词3--基于汉字成词能力的HMM模型识别未登录词

    作者:zhbzz2007 出处:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明.谢谢! 1 算法简介 在 结巴分词2--基于前缀词典及动态规划实现分词 博 ...

  6. .Net Core MVC 网站开发(Ninesky) 2.3、项目架构调整-控制反转和依赖注入的使用

    再次调整项目架构是因为和群友dezhou的一次聊天,我原来的想法是项目尽量做简单点别搞太复杂了,仅使用了DbContext的注入,其他的也没有写接口耦合度很高.和dezhou聊过之后我仔细考虑了一下, ...

  7. 用scikit-learn学习谱聚类

    在谱聚类(spectral clustering)原理总结中,我们对谱聚类的原理做了总结.这里我们就对scikit-learn中谱聚类的使用做一个总结. 1. scikit-learn谱聚类概述 在s ...

  8. javascript工厂模式和构造函数模式创建对象

    一.工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程(本书后面还将讨论其他设计模式及其在JavaScript 中的实现).考虑到在ECMAScript 中无法创 ...

  9. QQ空间动态爬虫

    作者:虚静 链接:https://zhuanlan.zhihu.com/p/24656161 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 先说明几件事: 题目的意 ...

  10. 使用HTML5的cavas实现的一个画板

    <!DOCTYPE html><html><head> <meta charset="utf-8"> <meta http-e ...