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,然后操作它的私有成员属性或者方法,来获取到调用栈信息进行跳转,最好还是避免使用这种方法吧。

ReactNative之使用LayoutAnimation创建动画

LayoutAnimation是官方提供的一个实现动画的API,但官方文档比较简单,结合查看源代码和demo试验,总结了一下如何使用。

最开始重点强调一下,android平台要使用的话,必须加上这段代码才行,否则像我一样用android设备调试,写了半天发现都不起作用。

var UIManager = require('UIManager');
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);

使用LayoutAnimation实现动画其实很简单,就是在setState之前调用LayoutAnimation.configureNext,然后在下次渲染时就根据新的state产生动画了,所以先写一个最简单的例子

class TestLayoutAnimation extends Component{
    
    constructor(props) {
        super(props);
        this.state = {left:100,top:100,width:100,height:100,isVisible:true}
    }	
    render(){
        return <View>
            <Button title="click" onPress={this._onClick.bind(this)}/>
            {
            this.state.isVisible ? 
            <View style={{position:"absolute", left:this.state.left, top:this.state.top,
					width:this.state.width, height:this.state.height, backgroundColor:"red",
				}}/>
             : null
            }
        </View>
    }
    _onClick(){
        LayoutAnimation.configureNext({
            duration:500,
            update:{
                type:LayoutAnimation.Types.spring
            }
        });
        this.setState({left:this.state.left+50,top:this.state.top+50,
        width:this.state.width+50,height:this.state.height+50});
    }
}

运行这个简单的demo就可以看到动画效果,就是由LayoutAnimation.configureNext产生的,我们可以通过看源代码知道这个API的使用规则。LayoutAnimation.js位于react-native\Libraries\LayoutAnimation\文件夹内,Java代码位于react-native\ReactAndroid\src\main\java\com\facebook\react\uimanager\layoutanimation文件夹内

configureNext方法接受的参数是一个Config对象,这个对象的规则是

type Config = {
      duration: number,
      create?: Anim,
      update?: Anim,
      delete?: Anim,
};

duration参数是必须的,指定这个动画的执行时间。 create,update,delete三个参数是Anim类型,都是可选参数。

  • create指定了一个View从不可见变成可见状态时执行的动画效果,这里的不可见,包括width,height为0,或者像demo通过this.state.isVisible控制了不渲染,不包括透明度为0的情况。如果没有create参数,那view出现时是没有动画效果的
  • update参数指定了View在可见状态时因为state变化产生的动画效果
  • delete和create相反,当一个View变为不可见时,指定其动画效果。

所以如果一个View在执行动画前是不可见的状态,则必须配置create参数,否则配置update参数就可以。如果消失时需要动画,则配置delete参数。

我们看Anim的格式要求

const animType = PropTypes.shape({
      duration: PropTypes.number,
      delay: PropTypes.number,
      springDamping: PropTypes.number,
      initialVelocity: PropTypes.number,
      type: PropTypes.oneOf(Object.keys(Types)).isRequired,
      property: PropTypes.oneOf(
        // Only applies to create/delete
        Object.keys(Properties),
      ),
});

其中type参数是一定需要的,而property参数则只在create和delete时需要。简单点的话,就只看type和property参数

  • type参数必须是LayoutAnimation.Types这个Enum中的值,可取的值有spring, linear, easeInEaseOut, easeIn, easeOut, keyboard 它们的具体效果可以自行通过demo测试
  • property必须是LayoutAnimation.Properties这个Enum中的值,可取的值只有两个opacity, scaleXY。不看源代码大概也能推测出它的意义就是View在出现或者消失时,是按scale还是opacity的效果来。

了解这些之后我们就可以实现改变View的尺寸,坐标的动画了,但对透明度,Transform的变动不会有动画效果

LayoutAnimation提供了一个接口Create方法,可以生成configureNext方法需要的参数Config

function create(duration: number, type, creationProp): Config {
      return {
           duration,
        create: {type,property: creationProp,},
        update: {type,},
        delete: {type,property: creationProp,},
      };
}

使用这个方法可以生成了一个比较简单的config,它把create,update,delete都实现了,属性只有type和property。

此外LayoutAnimation还提供了3个写好的动画效果可以直接使用,它们是LayoutAnimation.easeInEaseOut, LayoutAnimation.linear, LayoutAnimation.spring,看源代码就很容易明白,它们其实就是写好了代码实现的configureNext方法,所以使用起来就是在setState之前直接调用即可

LayoutAnimation.easeInEaseOut();
LayoutAnimation.linear();
LayoutAnimation.spring();

LayoutAnimation的底层实现是在native层,所以不会被js线程卡顿影响,比较适合做一些简单的动画,使用起来也很简单,但不太适合用来做组合动画。configureNext的第二个参数可以传入一个回调,通过这个回调函数可以开始新的动画,但只在ios平台才有效,如果需要实现复杂的动画,应该使用Animated类。

js之import与export复合

在重构项目时有时会有这种需求,从一个文件内import进来然后export出去。这里总结一下一些写法,在ECMAScript6 入门里有一段相关内容可做参考。内容很简单,基本就是语法规范而已。

首先是导出文件a.js

//a.js
let a = {"a":1}
export {a}

b.js需要导入a.js再导出,有一种比较简单的写法

//b.js
export {a} from "./a"

在使用的时候

//index.js
import {a} from "./b"

跟从b.js内正常export出来的一样对待。

还有一种写法效果一样,但有一点区别。就是

//b.js
import {a} from "./a"
export {a}

这种写法的作用是在b.js内可以使用变量a,而前一种写法不能。

如果a.js导出的比较多,一般会使用import * 来引用,也可以使用export * 来导出,例如

//b.js
export * from './a'
export let b = {"b":2}
//index.js
import * as B from "./b"
console.log(B.a); 

然后看一下export default的情况

//a.js
export default a = {"a":1}
//b.js
export a from "./a"					//(1)

export {default as a} from "./a"	//(2)

import a from "./a"					//(3)
export {a}
//index.js
import {a} from "./b"

第一种写法是错误的,第二种和第三种写法没问题,而且这两种写法在b.js内都可以正常使用变量a

也可以把一个普通的export转换为export default,例如

//b.js
export {a1 as default} from "./a"
//index.js
import a1 from "./b"

但这种方法在b.js内不能使用变量名a1,如果要使用的话,下面这样写就可以了

//b.js
import {a1} from "./a"
export default a1