ReactNative之手动实现一个Redux

最近我一直在考虑是否要移除项目中的redux使用,要做这个决定,首先是要搞明白redux的目的是什么,然后看看使用redux有哪些利弊。网上最适合了解redux的是redux中文文档

假设我们只有一个界面,那肯定不需要redux了,直接对this.state操作就行了,但如果有两个界面A和B,在A界面的操作需要改变B界面,在B界面的操作需要改变A界面,那么要么它们互相暴露接口,要么让它们把state放到共同的父组件里然后通过props传递下去,这样已经很复杂了,一旦有更多页面,它们之间互相影响,那就是一场噩梦了。要解决这个问题,就需要把数据和UI分离,所有界面渲染所需要的state,都存在一个大仓库里,这就是redux里的store。每个界面从仓库里取自己需要的state来进行渲染,当A界面的操作需要修改B界面时,它直接去修改仓库里B界面需要的数据,然后通知一下B界面刷新。在redux里是通过dispatch一个action来通知store修改数据的,如何修改则在reducer里实现,当reducer修改好数据后,会通知绑定了相关数据的界面执行setState进行界面刷新,这是通过react-redux提供的connect函数实现的。这就是redux做的所有事情,就仅此而已,我们可以很容易的自己实现,只需要实现一个数据中心,提供修改数据的接口,然后给页面提供监听即可。

为了避免描述不够清晰,以一个实际场景为例的话,我们有一些好友列表数据,那么我们实现一个FriendManager类,这个FriendManager做成全局单例对象,每个页面都可以调用它的接口。现在我们有个好友列表界面A,进入这个界面时还没有数据,我们需要显示loading界面,很简单,A界面直接来一个

this.state = {isLoading:true, friends:[]}

就行了,不用像使用redux时考虑哪些state放到store里管理,哪些自己管理。

然后调用接口去像服务器请求数据,例如

FriendManager.requestData();

为了收到数据后能够刷新界面,我们需要注册监听事件,例如

NotificationCenter.addListener(UPDATE_FRIEND, this._updateFriend);

数据回来了,存储在FriendManager内,然后触发监听通知A界面

NotificationCenter.trigger(UPDATE_FRIEND, data);

在A界面的回调函数里

_updateFriend(data){
    this.setState({isLoading:false, friends:data})
}

搞定,页面刷新了。现在我们有个好友详情界面B,在这个界面删除好友,这需要改变界面A,很简单,FriendManager提供一个接口

FriendManager.removeFriend=function(friendID){
    this._friends.splice(...);
    NotificationCenter.trigger(UPDATE_FRIEND, this._friends);
}

B界面调用这个接口,A界面就收到通知进行刷新了,不管AB界面隔得多遥远,也不管有多少个界面会互相影响,他们没有任何耦合。

redux基于这个核心目的做了一些优化,例如使用action来代替直接调用FriendManager内的方法,这样可以更加清晰明了,开发者很明确要做一件什么事情。但抽象成action是有代价的,开发者需要理解action的概念,然后需要实现很多生成action的方法。像redux三大原则(单一数据源,state只读,reducer为纯函数),再加上middleware, sagas,immutable.js等等,很多人就晕头转向了,这就是我认为的使用redux的弊端吧,我们的项目一开始就引入了redux,但搭了个框架后,大家都不用,又要写action,又要写reducer,还要去connect,就算写了,也不考虑immutable原则。所以还是看项目的实际情况吧,如果确实不是很复杂的场景,例如上面举的好友列表数据管理方案,其实自己实现倒更直观。

ReactNative之js与native通信流程(Android篇)

这篇文章简要介绍一下android平台,js和java互相调用时,经过的流程。

js调用java

要从js端调用java代码,需要把java端能被调用的接口,在js代码中进行注册。这个实现在react-native/Libraries/BatchedBridge/NativeModules.js文件中。

看NativeModules.js内的代码里有:

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
    NativeModules = global.nativeModuleProxy;
} else {
    const info = genModule(config, moduleID);
    NativeModules[info.name] = info.module;
}

这里把代码简化了一下,这段代码其实是说明了两种情况,当加载离线bundle时,genModule是从ReactCommon/cxxreact/JSCNativeModules.cpp里调进来的,否则是从ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp里把__fbBatchedBridgeConfig传进来然后解析的。具体的细节不用深究。总之java端有哪些接口可供调用,模块名函数名等信息传到js层,然后经过一层封装,实际调用了MessageQueue里的enqueueNativeCall方法。

在enqueueNativeCall方法里我们可以看到一个细节,就是js对原生的调用是被存储在队列里的,每5毫秒把这些请求集中交给原生处理并清空一次队列。从js开始进入原生代码的入口是

global.nativeFlushQueueImmediate(queue);

这个方法定义在ReactCommon/cxxreact/JSCExecutor.cpp中。它的实现是

m_delegate->callNativeModules(*this, folly::parseJson(queueStr), false);

这个m_delegate如果一路跟踪的话,就会发现是一个JsToNativeBridge类实例,这个类定义在ReactCommon/cxxreact/NativeToJsBridge.cpp中。JsToNativeBridge的callNativeModules方法实现为

m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId);

进入ModuleRegistry类的callNativeMethod方法中,发现执行的是NativeModule类的invoke方法,而继承自NativeModule类的有两个:一个是ReactCommon/cxxreact/CxxNativeModule.h,我们用它实现了js与C++的直接通信,这个以后有空再写。另一个是ReactAndroid/src/main/jni/react/jni/JavaModuleWrapper.h里的JavaNativeModule。JavaNativeModule的invoke方法通过JNI调用了java方法。

static auto invokeMethod = wrapper_->getClass()->getMethod<void(jint, ReadableNativeArray::javaobject)>("invoke");

然后我们去寻找JavaNativeModule类在java层对应的什么类。回到前面跟踪的路线,JsToNativeBridge的实例来自于NativeToJsBridge类的构造函数,而NativeToJsBridge的实例来自于Instance.cpp的initializeBridge方法,然后跟踪到CatalystInstanceImpl.cpp里的initializeBridge方法,这是个JNI方法,java层传过来的javaModules就是在这个函数里,被封装成了JavaNativeModule类。我们找到ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java里的initializeBridge方法,就知道C++中的JavaNativeModule对应了java中的JavaModuleWrapper类,这样最终我们就调用了JavaModuleWrapper类里的invoke方法。

java调用js

源代码中有一个很好的从java调用js的例子,就是ReactRootView里的代码

String jsAppModuleName = getJSModuleName();
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);

从CatalystInstanceImpl.java的getJSModule方法里我们能看出,所有java能调用的js接口,都是继承自JavaScriptModule类,我们可以在项目中搜一下,就能默认有哪些类可以调用了,例如我们最常用的DeviceEventManagerModule。在JavaScriptModuleRegistry类中通过动态代理创建JavaScriptModule,这块我不太了解,所以只能跳过去了。实际我们调用的是JavaScriptModuleInvocationHandler类的invoke方法,它调用了CatalystInstanceImpl.java内的callFunction方法。然后通过PendingJSCall类的call方法,调用了

catalystInstance.jniCallJSFunction(mModule, mMethod, arguments);

这是一个JNI方法,于是进入到了ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp内,然后来到ReactCommon/cxxreact/Instance.cpp的callJSFunction方法,然后是NativeToJsBridge的callFunction方法,再来到JSCExecutor的callFunction方法,在这里找到MessageQueue.js里的callFunctionReturnFlushedQueue方法调用。看messageQueue.js的__callFunction方法里,实现是

const moduleMethods = this.getCallableModule(module);

哪些模块能够被调用,来自于MessageQueue类的registerLazyCallableModule,在Libraries文件夹内搜一下,就能找到Libraries/Core/InitializeCore.js。到这里就顿时清楚了,这些类在Java层中以继承自JavaScriptModule的interface形式记录下来,在js层中具体实现,然后按照上述流程最终执行js代码。

ReactNative之VirtualDomTree的diff原理

以前在解决一次flatlist没有局部刷新的问题时,写了一篇博客里面提到了官方的一篇文档叫做reconciliation。前阵子有人问我react的virtualDomTree的diff算法是怎么实现的,有没做什么优化点。我知道是这篇文章里的内容,但当时却说不清楚,这让我觉得我对这篇文章其实理解的并不够,所以把它再看一遍,然后把自己的理解记录下来,但这并不是翻译,完全是按照我自己的理解来写的,并不会非常严谨,但应该不会有误,如果有不同看法欢迎讨论。

正常来说,Dom树的diff算法复杂度是O(n^3),如果页面很复杂时,性能就非常低了,比如有1000个节点的树,需要比较一亿次。React进行优化后,实际的复杂度降低到了O(n),它基于两个原则:

  1. 两个节点类型不同的话,以其为根节点的树也完全不同
  2. 通过节点的key属性,可以定位新旧Dom树上对应的节点,来判定是否需要rerender

首先看如何比较,假定我们已经确定了要比较的节点

  1. 如果节点类型不同,比如原来是Image,现在是View。那么以这个节点为根节点的整个Dom树都需要新建。它本身的属性,以及所有的子节点都没有比较的必要了。
  2. 如果节点类型相同,那么就比较它的属性,只有那些发生了变化的属性会被记录下来,然后进行更新,没有发生变化的属性也就保持不变。然后循环遍历它的所有子孙节点进行比较。

需要注意一下,一旦一个节点比较有了diff,也就是变得dirty,那么它本身以及所有的子孙节点,都会变成dirty。diff会生成,但是否触发re-render取决于具体实现。不要误认为只要有diff必然会导致re-render,或者只要没触发re-render就没有diff。

然后就是我们怎么确定某个节点在新旧Dom树上如何对应的,假设下面这种场景

//old
<View>
    <Text>1</Text>
    <Text>2</Text>
</view>
//new1
<View>
    <Text>1</Text>
    <Text>2</Text>
    <Text>3</Text>
</view>
//new2
<View>
    <Text>3</Text>
    <Text>1</Text>
    <Text>2</Text>
</view>

old指老的Dom树,new1在它的末尾插入了一个新的子节点,根据上面的原则,根节点View和Text1,Text2都没有变化,只是新增了Text3而已。但new2就不一样了,它在起始插入了一个Text3,这就导致Text1变成了Text3,Text2变成了Text1,然后新加了一个Text2,这显然是不太合适的,明明只增加了一个子节点,但三个都重绘了。例子中Text是一个很简单的组件,实际上它可以是一个非常复杂的根节点,那样的话可能就导致一整个Dom树的变动了。

解决这个问题的方案就是给Component添加了key属性。一个节点的所有子节点拥有一个唯一的key,注意这个唯一并不是全局的唯一,只需要跟它的兄弟节点区分开来就行。加上key之后再看上面的例子

//old
<View>
    <Text key="1">1</Text>
    <Text key="2">2</Text>
</view>
//new2
<View>
    <Text key="3">3</Text>
    <Text key="1">1</Text>
    <Text key="2">2</Text>
</view>

现在在进行diff时就知道了,key为1和2的Text节点内容没有变化,不会生成diff,只需要增加Text3就可以了。

文档里提到在项目开发中key的设置不要太过随意,例如直接使用index,如果这样,当子控件顺序发生变化时,可能就产生了额外的diff。我在使用flatlist时,keyExtractor直接使用index,导致在数组起始插入一个数据时,整个flatlist全部进行了刷新,而不是局部刷新,就是这个原因。

最后有两个结论:

  1. 如果没有必要,不要轻易改变一个节点的类型。也就是说显示效果没变,却改变节点类型。这在实际情况中很少发生。
  2. 使用一个稳定和唯一的key来让组件和它的兄弟组件区分,不使用或者不合理的使用可能造成性能问题。

ReactNative之ios平台bundle拆分实现

今天把放在github上的bundle拆分demo实现了ios版本,运行没问题后代码提交了上去,demo地址。因为之前没接触过ReactNative的ios端源代码,所以实现的过程其实就是把ios端的运行流程大概看了一遍。

如果不追究太多细节的话,整个流程就是AppDelegate.m里创建RCTRootView,在RCTRootView的init函数里,创建RCTBridge,然后在RCTCxxBridge的start方法里加载bundle并执行,加载完后回到RCTRootView的runApplication回调函数里,调用js方法AppRegistry.runApplication进入了js代码的入口。

android和ios端的bundle拆分和分步加载方案原理是一样的,原来的实现是创建一个View,然后加载bundle,加载好了展示界面。我们把步骤稍微改一下,先加载common.bundle创建好js context并存起来,然后当需要展示界面时,创建view,并用已有的js context来加载业务bundle。为了能自由指定Bundle加载的时机,我们需要把加载bundle的接口抽离暴露出来。

ios和android端流程上基本一样,只是类名字不同。android平台创建js context的类是ReactInstanceManager,而ios平台是RCTBridge,android平台的view是ReactRootView,而ios平台是RCTRootView等等。从代码量上看,android端稍微复杂一些。

ReactNative之混合Navigation跳转问题

首先把界面列出来

class Tab1 extends Component{
    render(){
        return <View><Text>tab1</Text>
                <Button title="totab2" onPress={
                    ()=>{this.props.navigation.navigate("tab2")}
                }/>
                <Button title="toscene2" onPress={
                    ()=>{this.props.navigation.navigate("scene2")}
                }/>
    }
}
class Tab2 extends Component{
    render(){
        return <View><Text>tab2</Text>
                <Button title="totab1" onPress={
                    ()=>{this.props.navigation.navigate("tab1")}
                }/>
                <Button title="toscene2" onPress={
                    ()=>{this.props.navigation.navigate("scene2")}
                }/>
    }
}
const TabNav = TabNavigator({
    tab1:{screen:Tab1,}
    tab2:{screen:Tab2,}
}})
class Scene2 extends Component{
    render(){
        return <View><Text>scene2</Text>
                <Button title="toscene1" onPress={
                    ()=>{this.props.navigation.navigate("scene1")}
                }/>
    }
}
const StackNav=StackNavigator({
    scene1:{screen:TabNav},
    scene2:{screen:Scene2}
})

这是最简单的情况,一个StackNavigator内的界面是TabNavigator,在Tab1和Tab2里,不论是进行TabNavigator还是StackNavigator内的跳转,都直接使用this.props.navigation.navigate即可。在注册生成TabNavigator和StackNavigator时给每个界面都注册了一个唯一的key,根据这个key可以在任意界面间跳转,例如在scene2界面,除了可以跳回scene1外,也可以指定跳回tab1或者tab2。

如果TabNavigator被包装在一个普通Component内,情况就稍微复杂一些,例如

class TabContainer extends Component{
    render(){
        return <View style={{flex:1}}>
            <Text style={{margin:20}}>tabContainer</Text>
            <TabNav />
        </View>
    }
}
const StackNav=StackNavigator({
    scene1:{screen:TabContainer},
    scene2:{screen:Scene2}
})

直接运行的话就会发现,在Tab1和Tab2界面之间的跳转没问题,但没法跳转到scene2了,解决方案是将<TabNav />替换成

<TabNav navigation={this.props.navigation}/>

然后添加一行

TabContainer.router = TabNav.router;

就和上面行为一样了,可以在各个界面自由跳转。上面这句话通过给TabContainer增加一个router属性,将一个普通Component变成一个navigator,所以就能跳转了。

还有一种办法是通过给TabNav设置screenProps的办法把this.props.navigation传到Tab1和Tab2里面去,代码就是

<TabNav screenProps={{navigation:this.props.navigation}}/>

在Tab1和Tab2里跳转到Scene2就可以

this.props.screenProps.navigation.navigate("scene2")

使用这个方案,在Scene2界面只能往StackNavigator的界面跳,不能像第一种方案一样直接跳到tab1或者tab2,所以不够灵活,推荐使用前一种方案。

ReactNative之一次FlatList无法局部刷新的bug修复

今天发现项目中一个奇怪的问题,在使用FlatList时,每个单元行Component明明实现了shouldComponentUpdate,但是当增加一行时,还是所有的单元行都重新render了,最后找到了原因。代码中FlatList实现的keyExtractor非常简单,因为每个单元行数据的key要求是唯一的,所以直接使用了index返回

  _keyExtractor(item, index){
    return ""+index;
}

然后在增加数据时,又是把数据插到了数组的最前面

let data = this.state.data;
this.setState({data:[{num:key,key}].concat(data)})

这样就出问题了,对FlatList内的每个单元行组件CellRenderer来说,它的props包括keyExtractor给出的key和data给出的数据,假设原来数据是[0,1,2,3],那么FlatList内的组件就包括

<CellRenderer key="0", num=0 />
<CellRenderer key="1", num=1 />
<CellRenderer key="2", num=2 />
<CellRenderer key="3", num=3 />

在数组最前面加上一个数据101后,FlatList内的组件就变成了

<CellRenderer key="0", num=101 />
<CellRenderer key="1", num=0 />
<CellRenderer key="2", num=1 />
<CellRenderer key="3", num=2 />
<CellRenderer key="4", num=3 />

我们在单元行组件里的shouldComponentUpdate实现是:

shouldComponentUpdate(nextProps){
    return this.props.num !== nextProps.num;
}

所以很显然已有的4个CellRenderer因为num变化,就全部刷新了。

找到原因后,要解决就很简单了

_keyExtractor(item, index){
    return item.num;;
}

所以结论就是:keyExtractor应该根据实际情况根据item数据来设置,不要贪图简单直接使用index

最后做了下验证,keyExtractor使用index时,将数据加在数组最后,而不是插在最前,那么没问题,不会全部刷新,因为前面CellRenderer的props都没有变化。但实际项目中不要贪图省事,之所以FlatList提供这个接口让开发者去实现,就肯定有这个需要,随便返回一个index可能就把自己给坑了。

关于re-render的原理,官方有一篇文章叫reconciliation讲的很清楚,看完就更容易理解这个问题的本质了。

ReactNative之在项目中使用TypeScript

最近在网上找到个开源的控件,但是源代码是用TypeScript实现的,放到项目里无法直接使用,于是google了一下怎么在ReactNative项目内使用TypeScript,然后找到了一个很简单的解决方案,试了一下没有问题。

首先安装react-native-typescript-transformer模块

yarn add --dev react-native-typescript-transformer typescript

然后在项目的根目录下创建一个文件 rn-cli.config.js

module.exports = {  
      getTransformModulePath() {
        return require.resolve('react-native-typescript-transformer')
      },
      getSourceExts() {
        return ['ts', 'tsx'];
      }
}

在项目根目录下创建一个文件tsconfig.json

{
      "compilerOptions": {
        "target": "es2015",
        "module": "es2015",
        "jsx": "react-native",
        "moduleResolution": "node",
        "allowSyntheticDefaultImports": true
      }
}

然后就可以放心在项目里写TypeScript代码了,例如项目中ts目录下有test.ts文件,我们在import这个文件时,就像import一个js文件就可以了

import './ts/test'

这些以ts,tsx为后缀的TypeScript代码文件会被转换成js文件,我们实际import的是转换后的js文件。

ReactNative之listView优化方案

ReactNative的ListView一直都因为性能问题饱受诟病,从源代码可以看到,它的主要问题是没有单元行重用机制,而且屏幕外的单元行不会被销毁,所以当ListView内容越来越多时,就会占用越来越多的内存,也越来越卡。针对这个问题,目前有几种解决方案。

ListView

ListView使用时可以有一些优化方案的。首先通过initialListSize和scrollRenderAheadDistance属性指定初始时单元格数量,可以加快初始化的速度。其次通过dataSource的rowHasChanged接口可以减少单元行re-render的次数。

FlatList

这是官方推出的解决方案,FlatList的思路是减少渲染的单元行数量,它在render时进行计算,只渲染屏幕中和缓冲区内的单元行,其余地方使用空白代替,这样不论FlatList有多少内容,实际渲染的单元行数量基本保持不变。因为有的单元行并没有渲染,当快速滑动到这个区域时,渲染是异步的,此时就会看到白屏,然后才开始显示内容。

使用FlatList时也有一些优化方案,首先是单元行组件如果使用PureComponent可以大大减少render的数量。其次实现props.getItemLayout接口可以避免临时测量每个单元行的尺寸,大大提高性能,如果能明确每个单元行的尺寸就一定要实现此接口。

initialNumToRender属性默认为10,它设定初始时渲染的单元行数量,这些单元行会常驻内存不被销毁,目的是为了scrollToTop时没有白屏。

maxToRenderPerBatch属性默认为10,它设定了在计算渲染单元行数量时每次处理的行数,这个数值如果太大可能导致渲染的单元行较多,占用内存以及增加白屏时间,如果太小了则会增加setState的次数

windowSize属性指定了屏幕外的区域渲染多少个屏幕单元(visible length),默认是21,它也会影响初始渲染的单元行数量。假如一个android设备高度为640,减去20像素的状态栏,一个屏幕单元是620,会额外渲染20个。这个数字如果比较大,则同时渲染的单元格数量会比较多,也增加了初始化的时间,如果比较小,则会增加出现白屏的几率。

SGListView

SGListView的原理是通过onChangeVisibleRows接口,当单元行滑动到屏幕外时将渲染内容变成一个空白View,当滑动到屏幕内时变回实际内容。这样因为屏幕外的单元行都是空白view,所以优化了内存占用。 enhancedListView也是类似的思路,但实现有点简陋。这个解决方案也会有白屏问题,实际上只要单元行的内容变掉,重新要渲染时,因为渲染是异步的,就都会有白屏问题。

LargeList

LargeList的想法是在js层实现了单元行复用。首先它和FlatList一样有白屏问题,因为渲染是异步的,在js层实现单元行复用,要求一个View渲染某些内容到真正展示出来,这段时间里屏幕就是白的。其次复用的作用是减少了创建单元行的消耗,这个消耗在整个ListView的性能消耗里并不占大头。最后使用ref持有Component引用并进行操作实际上不是RN推荐的一种处理方式,在复杂场景下很可能出问题。所以我不是很推荐使用它,实测也有不少bug。

RealRecyclerView

RealRecyclerView是封装了Android的原生控件RecyclerView,通过接口绑定同步原生view和js组件的内容。自己封装原生组件可能是难度最大的一种方案,因为有很多坑需要填,而且Android和iOS平台下风格也会不一致。但如果弄好了,就是真正实现了单元行复用的方案。像一些大厂的技术团队比如去哪儿就封装了自己的原生listView。 github上还有一个react-native-native-listview是同时封装了Android和iOS平台,可供参考。

ReactNative之PureComponent

首先把官方文档对于PureComponent的介绍搬过来,如果看明白了,就可以直接结束本文了:)

React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

React.PureComponent’s shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

Furthermore, React.PureComponent’s shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also “pure”.

PureComponent和Component的唯一区别就是shouldComponentUpdate方法

//Component
shouldComponentUpdate(nextProps, nextState){
    return true;
}
//PureComponent
shouldComponentUpdate(nextProps, nextState){
    return this.props !== nextProps || this.state !== nextState;
}

当props或者state发生了变化时shouldComponentUpdate会被调用,如果返回true则触发re-render,否则不会。 这里说的发生了变化,不一定是指内容或者引用发生了改变,只要调用了this.setState就认为是发生了变化,而只要父组件触发了re-render,就认为props发生了变化。

Component采用的默认实现是直接返回true,意味着只要props或者state发生了变化就会re-render。
PureComponent则是进行了一次浅比较(shallow comparison),只有当props和state之一在引用上发生了变化,才会re-render。

PureComponent相比Component减少了re-render的可能性,所以一定程度上可以优化性能。一个很明显的例子就是在使用flatList的时候。假设flatList当前渲染了第0-200个单元行,滑动后需要渲染第0-201个单元行,这是通过flatList的setState来刷新的,因为父组件flatList触发了re-render,所有的子元素也就是单元行组件,都会触发shouldComponentUpdate。如果单元行组件继承自Component,那么第0-200个单元行都会触发re-render,总共201次render。但如果继承自PureComponent,那么只会触发第201个单元行的render,总共只有1次render。这可以通过写一个小demo进行验证。

PureComponent虽然可以减少re-render,但也有坑,那就是它在shouldComponentUpdate里进行的是浅比较,也就意味着如果props和state是一个复杂对象的引用,那么它的内容变了但是引用本身没变,此时可能需要触发re-render却没有触发。

PureComponent适合用于props和state比较简单的组件,否则的话应该使用Component并重写componentShouldUpdate方法,既能减少re-render,又能避免错过re-render。

ReactNative之react-navigation使用

react-navigation是官方推荐的导航功能库,这里稍微总结一下如何使用它进行界面切换,以及一些细节问题。详情可以查看官方文档

一般使用系统自带的空间StackNavigator来进行界面切换。从名字也可以很直观的看出,这个导航的类就像一个stack,push一个新的界面上来,或者pop一个界面出去,当然也可以跳转,一次pop多个界面。

初始化

let Nav = StackNavigator(RouterConfigs, StackNavigatorConfig)

RouteConfigs是一个Object,用于注册所有可以跳转的界面。如果需要跳转的界面比较多,可以写一个脚本来生成,每个key-value形式为

screen1: {
    screen:MyComponent,
    path:xxx,
    navigationOptions:({navigation})=>({
        title:xxx
    })
}

screen对应Component类的名称,navigationOptions是一个回调函数,它会在每次界面被push时调用。它的参数是

{navigation:xxx, navigationOptions:xxx, screenProps:xxx}

我们可以从这个参数中解构出navigation,获取很多有用的信息,比如传入的参数navigation.state.params等等。然后返回一个Object,这个返回的Object被称为Screen Navigation Options,它用来设置一些UI属性,比如当前界面标题文字,标题栏样式等。

StackNavigatorConfig是一个Object,它可以指定初始显示哪个界面,可以指定整个StackNavigator通用的样式等。例如我们希望所有的界面都不需要显示标题栏,那么在这里设置headerMode为none即可。

跳转

每个在RouteConfigs里注册了的screen都是一个Component类,它的props都会自动多了一个navigation属性,也就是

this.props.navigation

通过它可以进行界面跳转,回跳,获取参数等操作。

  • navigate 这是一个function,通过它来进行界面跳转。 形式为 navigate(routeName, params, action),也就是push一个在RouteConfigs里key为routeName的界面,传递参数为params。 每次navigate一个界面都是新生成界面然后push,不会重用stack里已有界面。
  • state 这是一个object, 它的内容为{routeName:xxx, key:xxx, params:{xxx}} 这里routeName就是当前界面在RouteConfigs里注册时的用的key(也就是示例中的screen1), key则是系统自动生成的一个属性,这个key在使用goBack()函数指定跳回到某个界面时需要用到。params则是跳转时传入的参数,所以我们使用this.props.navigation.state.params来获取传入的参数。
  • setParams 这是一个function,使用它改变传入的参数
  • goBack 这是一个function,通过它返回到之前的界面,如果不带参数,则默认为退出当前界面回到上一层,如果参数为null,官方文档说go back anywhere, without specifying what is getting closed,看起来有点奇怪,不明白go back anywhere是不是任意跳转,但通过demo测试发现和不带参数表现是一样的。如果传入参数,则表示从传入参数代表的界面网上跳转一层,**注意这个参数不是RouteConfigs里的key(即screen1),而是上面state里的那个key(即this.props.navigation.state.key)**,因为只能获取当前界面的this.props.navigation.state.key,所以在A界面回跳需要B界面的key时,需要把B界面的key存起来或者传递给A。
  • dispatch 这是一个function,用来发布一个action,这个接口用的不多,属于比较深入的用法,具体查看官方文档。

使用

上面代码里生成的Nav本身是一个Component,所以不要把它想复杂了,就当做一个普通的Component来使用就可以了。

如果把Nav传给AppRegistry.registerComponent来作为起始Component。那很简单,在RouteConfigs里注册的各个界面里使用this.props.navigation进行操作就可以了。

如果作为一个普通Component使用,它的父容器内其它component想要进行navigator跳转,则通过它的ref来进行操作。例如

render(){
    return <View>
        <Button onPress={()=>{
            this._ref && this._ref.dispatch(
                  NavigationActions.navigate({ routeName: someRouteName })
            );
        }}/>
        <Nav ref={(c)=>this._ref=c}/>
    </View>
}

其它例如TabNavigator,DrawerNavigator的使用,大体和StackNavigator类似。其它高端的操作例如自定义Navigator,自定义Route等都参考官方文档。

回跳多个界面的解决方案

老版本的navigator可以通过routes列表获取当前的界面栈,也有popToRoute(),popToTop()这样的接口可以直接跳转。而react-navigation则没有界面栈的信息,只能通过goBack()传入一个key来指定跳转,这个key还只能获取到当前所在界面的,没法获取其他界面的key。如果要回跳多个界面,一个解决方案就是在需要回跳的目标界面获取key,通过props一路传递下来,然后在跳转界面使用。例如

//A.js
toB(){
    let key = this.props.navigation.state.key;
    this.props.navigation.navigate("B", {returnKey:key});	
}
//B.js
toC(){
    this.props.navigation.navigate("C", 
    {returnKey:this.props.navigation.state.params.returnKey});
}
//C.js
back(){
    this.props.navigation.goBack(this.props.navigation.state.params.returnKey)
}

这里从A传入key,在C界面跳转,注意并不是跳转到A界面,而是从A界面离开,调到A之前的一个界面。如果不通过传递的话,也可以把key存成全局变量,这样可以比较简单的实现回跳多个界面。还有一种hack的手段,就是获取navigation的ref,然后操作它的私有成员属性或者方法,来获取到调用栈信息进行跳转,最好还是避免使用这种方法吧。