浅析react-native的Navigator组件02之数据传送

前言:【初级内容】之前已经展示了一个最简单的Navigator的用法,虽然的确满足了页面弹来弹去的效果。但在实际使用的时候,我们会有各种各样的额外需求,比如页面与页面之间的数据怎么传递、页面跳转动画是否可以更换等。接下来就一步步整这些额外的东西。

renderScene传数据版

demo: 链接 (基于0.17,ios)

之前的 renderScene代码是这样:

1
2
3
4
5

renderScene={(route, navigator)=> {
let Component = route.component;
return <Component navigator={navigator}/>
}

然后push下一页的代码是这样的:

1
2
3
4
5
6
const { navigator } = this.props;
if(navigator){
navigator.push({
component:Secondpage,
})
}

就简单的return了一个包含了navigator参数的组件,想要跳转页面,那么你push什么组件(页面)给我,我就跳转什么组件。但如果想要额外传数据就只能干瞪眼。想要传数据怎么办,其实只需要加很少的代码就能实现,如下:

1
2
3
4
renderScene={(route, navigator)=> {
let Component = route.component;
return <Component {...route.params} navigator={navigator}/>
}

这是直接抄用的react-native中文论坛的高手帖子的内容 链接 。虽然看似简单的点点点,牵涉的知识有点复杂,不好理解。但假如你不去理解背后的原理,就记住这个固定格式,用起来一样的非常方便。初始化代码修改完毕,push时传数据是这样的:

1
2
3
4
5
6
7
8
9
10
11
const { navigator } = this.props;
if(navigator){
navigator.push({
component:Secondpage,
params:{
id:1
info:'from firstpage'
}

})
}

添加了一个叫做params的对象,对象里面随便放东西。放的东西怎么取呢,比如之前那个是从第一页跳转到第二页时传递过来的,在第二页我们这么取:

1
2
3
this.props.id

this.props.info

就这么简单,params的对象里面用什么key存储的内容,就直接this.props.key调用就行了。在demo里面添加了从第一页跳转第三页的按钮,可以看得见,一跳三,和二跳三的时候,这个第三页展示的props.id和props.info的值是不一样的。
当然,你不想传数据,不写哪个params对象也完全没有任何问题。

三个点的故事

以下是阐述原理时间,如果只想速度知道navigator传值的用法,可以不用往下看了。想知道背后的原理,请继续(希望我能讲清楚)

其实,这里如果你把

1
return <Component {...route.params} navigator={navigator}/>

改成

1
return <Component params={route.params} navigator={navigator}/>

也是能用的,push的格式也不用变,只是在调用的时候需要多写点单词,就是 this.props.params.id和 this.props.params.info。大家可以看得出来,是为了少写单词,所以就改成了上面那个三个点的格式。看起来更简洁(装逼的说法就是更优雅),但通常这种语法理解起来会更困难。下面我尝试一下,把这个玩意儿给说清楚。

三个点是什么

首先,三个点在ES6中,有两个语法都使用了这三个点。

  • Rest parameters (剩余参数)

    如果一个函数的最后一个形参是以 … 为前缀的,则在函数被调用时,该形参会成为一个数组,数组中的元素都是传递给该函数的多出来的实参的值.

    1
    2
    3
    4
    function f(a,b,...args){
    console.log(args);
    }
    f(1,2,3,4,5,6)

输出为

1
3,4,5,6  //这个好理解,就是传进来的参数去掉前面两个代表a和b的,剩下的作为一个数组丢给args。
  • Spread operator (展开运算符)

    展开运算符允许一个表达式在某处展开,在多个参数(用于函数调用)或者多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方就会这样。
    大部分情况是展开数组

    1
    2
    3
    function f(x, y, z) { console.log(x+y+z)}
    var args = [0, 1, 2];
    f(...args) //直接把args数组展开成类似f(0,1,2)这样的东西,输出为3
1
2
var parts = ['shoulder', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes']; // lyrics的值为 ['head', 'shoulder', 'knees' , 'and', 'toes']

组件传的三个点又是啥

那么, <Component {…route.params} navigator={navigator}/>这里面的3个点,是展开运算呢还是剩余参数呢?这个就需要知道组件这种语法格式到底代表了什么。

1
<Component  navigator={navigator}/>

这样的组件语法,是个标准的JSX写法,它可以翻译为标准的JS语句,翻译过来是这样的:

1
React.createElement( Component   , { navigator :  navigator })  //

又如

1
2
<Component  navigator={navigator}  params={route.params} />   //包含了两个参数的组件
React.createElement( Component , { navigator : navigator, params:route.params }) //这是翻译过来的JS语句

也就是说, createElement的第二个参数就是一个对象,里面有一堆键值对,每个键值对在具体组件里面就能使用”this.props.键名”来调用所对应的值。
然后我们的重点:

1
<Component {...route.params}  navigator={navigator}/>

翻译过来是这样:

1
React.createElement(Component, Object.assign({},  route.params , {  navigator : navigator  }));

后面这句 Object.assign({}, route.params , { navigator : navigator }),它的意思就是把route.params这个对象的内容,还有后面那个 { navigator : navigator }给组合到一起,形成一个新对象。
比如 route.params的对象为{id:1,info:”from first page”}。那么上面那句话再翻译一遍,就是:

1
React.createElement(Component, { id:1, info:"from first page", navigator :  navigator } );

这下明白了吧,为什么跳转之后,组件可以直接用props.id的形式,拿到上一个组件push过来的route.params中的id的值。

那么,回答问题,组件里面这三个点是扩展运算呢还是剩余参数呢?答案都不是,是JSX看到ES6的三个点的用法之后,自己实现的一个叫做Spread Attributes(展开属性)的语法。是不是有种被骗的感觉……嗯,我当时谷歌了半天的扩展运算和剩余参数,最后在React的JSX文档上看到这个解释,就是这种感觉。反正再复习(学习)一遍ES6的语法也不是坏事,是吧 :)。

唔,又查了一会资料,组件三个点扩展对象不是JSX的发明,而是ES7的提案,叫做rest/spread Properties,专门用于对象的。在RN上可以用这个语法。用法和上面针对数组的rest和spread差不多,仅仅是把外面的中括号(数组表示)变成大括号(对象表示)而已。比如

1
2
3
4
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };///rest
x; // 1
y; // 2
z; // { a: 3, b: 4 }
1
2
let n = { x, y, ...z };//spead
n; // { x: 1, y: 2, a: 3, b: 4 }

不过有点不太一样的是,数组扩展的时候,是不会有重复的,它的下标会都不一样。但对象扩展的时候会遇到key重复的情况。这种情况,语法处理结果是后面的key的值覆盖前面的。如:

1
2
let n = {x:1, y:2, z:3}
let m ={...n, z:4} // m={x:1, y:2, z:4}

参考链接:
展开运算符
剩余参数
JSX之扩展属性

浅析react-native的Navigator组件01之最简实例

前言:【继续啰嗦的初学者内容】个人认为在RN里面Navigator和Listview算是这套框架中最为重型的两个组件,也是非常常用的组件,同时也是坑比较多的组件。这两个搞清楚点,可以少走很多弯路。所以我打算把这两个组件的细节挖得清楚点,到时候有啥记不起的还可以回来看看。但水平有限,只能比较简单的挖,基本上是边测边挖,大致看看源码,像源码剖析这种深挖还搞不出来。所以初学者看看就好,大牛帮忙纠错……

Navigator原理

这个其实在之前组件生命周期就提到过一点,它的大概原理就是在初始化的时候新建了两个对象,一个是route(路由)数组,一个是navigaotor(导航)对象。前者用于保存一堆页面组件的信息,后者集成了一大堆方法来对页面组件信息进行处理(push、pop、replace等,见之前的文章)。有了这两个对象,我们就能完整的控制页面的跳转。所以初始化完毕之后,需要把两对象都给传到下一个页面组件,这样下一个页面就能继续通过这两个对象来控制页面跳转。另外,在初始化时,还需要指定页面跳转的动画特效,这个之后再细讲。一般来说,我们就只需要这么一次初始化,之后就尽管调用传进来的navigator进行操作就行了。

最简单的Navigator实例

demo在github
其实就是之前生命组件那个demo改的(基于RN 0.17,只写了ios版,不过安卓的把index.ios.js文件内容复制过去,应该通用)。该实例一共有4个js文件:

  • index.ios.js

    1
    RN入口,对Navigator进行初始化,初始化后会自动跳转指定的firstpage
  • firstpage.js

    1
    第一页,只有一个按钮,就是能push到第二页
  • secondpage.js

    1
    第二页,两按钮,一个push到第三页,一个pop回第一页
  • thirdpage.js

    1
    第三页,一按钮用于pop回第二页

浅析初始化

后面123页的跳转和返回的代码非常简单,通过this.props获取传过来的navigator对象,然后调用里面的push或者pop方法进行跳转和返回。push的参数为一个route数组中的route对象,比如{component:Secondpage},这个Secondpage为导入(import)的实际页面对象名称。稍微复杂点是初始化代码,即index页面调用组件的内容。

1
2
3
4
5
6
7
8
9
10
<Navigator
initialRoute={{component: Firstpage}}

configureScene={(route)=> {return Navigator.SceneConfigs.VerticalDownSwipeJump;}}

renderScene={(route, navigator)=> {
let Component = route.component;
return <Component navigator={navigator}/>
}}
/>

组件中有3个参数。

  • 第一个是initialRoute,大意是初始化route,需要提供一个route对象(代表某个指定页面),用于初始化完毕之后自动跳转该页面。
  • 第二个参数叫做configureScene,大意为配置组件(的特效),就是我们之前说的,需要指定一个页面跳转的特效动画,这个之后细讲,现在知道反正这么写就是了。
  • 第三个参数renderScene,大意为渲染组件(或生成组件),接受一个回调函数,这个回调函数最终需要返回一个组件,这个组件就是我们跳转之后的页面组件。回调函数会使用两个参数,这2个参数就是之前说到的,route对象(注意不是数组,仅仅是类似{component: Firstpage}这种样式的对象)和navigator对象。从route对象里面取出对应的组件(component),然后直接返回这个组件,同时把navigator对象给传进去。

这段代码的运行流程,你可以大致这么理解:先初始化route数组和navigator,然后从initialRoute的收到第一个页面信息,再从configureScene收到跳转动画信息,再把initialRoute页面信息丢给route数组,再从renderScene获取到渲染组件的方法,把页面信息和navigator都丢给这个方法,进行初始化后的自动跳转。当然真正的细节不可能那么少,能帮助理解这个初始化信息就行。

好了,最简单的navigator就是这样,之后会慢慢的把它给搞复杂点,以承载更多的需求。

react-native组件生命周期在navigator上的表现(续)

前言:(依旧是啰嗦的初级内容),本来我以为之前的测试对Navigator的用法和生命周期已经比较明了了。后来因为某个原因我看了一下Navigator的代码实现,发现人家Navigator提供一大堆的跳转方法,不仅仅是push和pop……我竟然之前一直没想过它还有其他方法,只是隐隐觉得这2个方法是不是少了点,很多需求都不好实现。结果只是我知道太少而已(一看就是没写过完整应用的人)。下面就顺便说一下这些方法,再简单解释一下它们关联的页面的生命周期。

routestack数组

在介绍跳转方法之前,先简单说说Navigator实现页面跳转的原理。在其实现代码里面,有个贯通全局的routestack(路由栈)数组。这个数组里面保存了跳转的页面信息。接下来要介绍的一大堆方法,都是围绕它来实现的,比如push方法就是在原有基础上再加载一个页面,然后把这个新页面的信息给添加到数组的尾部。pop则是销毁当前的页面,同时在数组尾部把这个被销毁的页面的信息给去掉。这都属于典型的栈(stack)式数据结构用法,所以取名为routestack。

然后Navigator提供了一个方法,可以返回一个路由栈数组给我们,当然这个返回的数组的信息肯定没有内部实现的那么多,但也是很有用的。
方法:

1
getCurrentRoutes()

基于我上次的demo: github, 从第一个页面push到第二个页面,再push到第三个页面,在第三个页面上返回的路由栈数组长这样:

1
2
3
4
5
[
{ name: 'firstpage', component: [Function: Firstpage] },
{ name: 'secondpage', component: [Function: Secondpage], params: { info: 'from firstpage' } },
{ name: 'thirdpage', component: [Function: Thirdpage], params: { info: 'from secondpage' } }
]

pop方法们

关于push的方法只有一个,但pop类的方法除了pop本身以外,还有以下几个:

  • popToTop()
  • _popN(n)
  • popToRoute(route)
  • resetTo(route)

其实看名字都知道作用了,大概解释下。
另外如果解释有举例,都是基于这个假设:当前路由栈数组有[1,2,3]共3个页面(数组里面的123分别代表一个route对象,看懂意思就行),当前页为3号页面。

1
popToTop()  //直接回到第1个页面,其他页面全部销毁(组件unmount,生命到了尽头),例子中路由栈最后变成 [1]
1
_popN(n)  //n为数字,返回到前n层页面的意思。n为1,就和pop()方法是一样的,n不能大于当前路由栈数组的长度减一。 这是个内部方法,但感觉某些场景可能有用(比如想直接回退两层),但有风险,n出错程序就要崩。返回经过的那些页面也是全部unmount。
1
popToRoute(route)  //回到路由栈的route对象的那一页。但这个route对象不能像push那样直接写类似{ name: 'firstpage', component:Firstpage }这样的对象字面量,而是只能用 getCurrentRoutes()返回的数组里面去挑一个出来,也就是说只能用对象栈已有的route对象。
1
resetTo(route) //先咔嚓掉第一个页面,然后新建一个route指代的页面放在第一个页面的位置,再把其他剩余的页面全部干掉。比如我 resetTo(2),路由栈数组由[1,2,3]变成[2,2,3],最终变成[2]。这个其实严格说来不算pop方法,但他咔嚓的页面也不少,就暂时归pop一类好了。

replace家族

我之前嫌弃Navigator方法少的时候,就在嘀咕好歹给个replace的方法嘛,要不然某些应用场景还真没法实现。replace的方法有如下4个:

  • replace(route)
  • replacePrevious(route)
  • replacePreviousAndPop(route)
  • replaceAtIndex(route, index, cb)

他们的功能,主要是对当前路由栈的页面进行替换。

1
replace(route) //把当前页面给销毁掉,然后新建一个route指代的页面来代替当前页面,注意这个过程是没有过场动画效果的。比如我触发replace(2)方法,路由栈从[1,2,3]变成[1,2,2]。这个最常用的地方应该是自己替换自己(相当于重载页面)。
1
replacePrevious(route) //把上一页给销毁掉,新建route指代页面代替它,没动画,你只有pop回去的时候才会发现已经被改了。比如触发 replacePrevious(1),路由栈会从[1,2,3]变成[1,1,3]。
1
replacePreviousAndPop (route) //在前面方法的基础上,帮你pop回去……
1
replaceAtIndex(route, index, cb) //把index指的那一页给销毁,并用route指代的页面来代替。其实在Navigator的实现代码里面,上面2个方法就是用这个方法加工了一下。那个cb参数不知道有什么用,实现代码也没调用。

jump团伙

之前那些方法,都是伴随着页面组件的各种生老病死,仔细想想还是很残酷的,接下来的方法讲究以活为贵,不会打生打死,比较简单,我就名字和解释放一堆了。

1
jumpBack() //跳转上一页,当前页面组件不销毁,而且不会触发任何更新动作,与之对比,pop是销毁当前组件,且触发上一页的组件更新。比如路由栈[1,2,3],运行jumpBack(),页面跳到2号页面,但路由栈依然是[1,2,3],活动页面为2号。
1
jumpForward()//跳转后一页,前提是有后一页可以跳。比如之前运行了jumpBack(),再运行jumpForward(),就可以又从2号页面跳回3号页面,路由栈继续是[1,2,3]。
1
jumpTo(route)//跳转指定页面,这个route参数和之前那个 popToRoute是一样的,不能指定对象字面量,只能从当前路由栈里面取。

react-native组件生命周期在navigator上的表现

前言:

本文为初级内容,仅限于刚入门的爱好者看,大牛就不用关注了。关于react-native(RN)的组件生命周期,网上好多高手都已经讲过不少了。看过的的都知道,组件生命周期分为三大部分:挂载(mount)组件部分(包括初始化和挂载)、挂好之后的循环更新部分(即根据state和props的变化循环刷新组件显示)、卸载(unmount)组件部分。知道是一回事,但在具体实现中,这组件何时挂载,何时卸载呢?下面我就用具体实现中最常用的navigator的表现来加深下理解。

测试代码demo地址(RN版本1.7,仅写了ios上的方法):github
这里先偷一张别人的组件生命周期示意图(图片来自链接),很清晰的展示了各个阶段,还有相关的关联方法。

demo代码简介

我这个代码写得很简单,使用navigator组件,然后创建了3个页面(first,second,third),点击页面的按钮进行跳转,除了back采用的navigator.pop外,其他跳转都是使用的push方法。然后每个页面(包括初始化navigator的index.js)都把组件各个阶段可能调用的方法都写在里面进行log输出。如果嫌弃文字太多,可以直接看结论部分。

挂载组件部分

包括组件的初始化,即将加载,渲染(render)界面,加载完毕4个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor(props){
super(props);
console.log('first init');
}

componentWillMount(){
console.log('first componentWillMount is here');
}

render(){
console.log('first render is here')
}

componentDidMount() {
console.log('first componentDidMount is here');
}

循环更新部分

包括即将接受变动的属性、是否更新组件(这个必须返回true,否则后面就不用玩了)、即将更新组件、更新渲染、更新完毕这5个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
componentWillReceiveProps(nextprops){
console.log('first componentWillReceiveProps is here');
}

shouldComponentUpdate(){
console.log('first shouldComponentUpdate is here');
return true;
}

componentWillUpdate(){
console.log('first componentWillUpdate is here');
}

render(){
console.log('first render is here')
}

componentDidUpdate(){
console.log('first componentDidUpdate is here');
}

卸载组件部分

只有即将卸载组件这一个方法。

1
2
3
componentWillUnmount(){
console.log('first componentWillUnmount is here');
}

开始初始化

编译运行,模拟器显示first页面,此时的输出为

1
2
3
4
5
6
7
8
index init
index componentWillMount is here
index render is here
first init
first componentWillMount is here
first render is here
first componentDidMount is here
index componentDidMount is here

编译运行,程序会先调用index初始化navigator,然后自动从navigator跳转到预定好的first页面。
我们可以从输出部分看出,首先是index初始化数据,然后index准备加载组件,然后index开始渲染(render),在这个渲染代码中开始执行navigator的相关代码,navigator开始启动first页面。
接下来是first组件的初始化、准备加载、渲染、加载完毕。
first加载完了,才轮到index的渲染代码返回完毕,所以最后是index组件加载完毕输出。

此时组件存活情况:

  • index组件:存活,等待更新。
  • first组件:存活,等待更新。
  • second组件:未初始化。
  • third组件:未初始化。

push跳转

我们点击first页面上的goto second按钮,通过push方法,跳转到下一页(second),此时的输出为

1
2
3
4
5
6
7
8
9
first componentWillReceiveProps is here
first shouldComponentUpdate is here
first componentWillUpdate is here
first render is here
second init
second componentWillMount is here
second render is here
first componentDidUpdate is here
second componentDidMount is here

我们从输出部分看出,首先是first页面走了一遍更新组件的方法,到渲染(render)的时候,开始进入push方法加载第二页(second)。
接下来就是second的初始化、即将加载、渲染。
然后是first的更新完毕。
最后是second的加载完毕。

最后为什么是first更新完毕再轮到second的加载完毕,这点我也不太清楚。有明白的高手解释解释吗?

此时组件存活情况:

  • index组件:存活,等待更新。
  • first组件:在更新一次后,进入存活等待更新状态。
  • second组件:初始化后,进入 存活等待更新状态。
  • third组件:未初始化。

然后我在第二页(second)上点击了跳转第三页(third)页面,输出的内容和上面是一样的,只是组件名称发生了变化(first改成了second,之前的second的改成了third),就不贴了。此时的组件存活情况:

  • index组件:存活,等待更新。
  • first组件: 存活,等待更新 。
  • second组件: 在更新一次后,进入存活等待更新状态。 。
  • third组件: 初始化后,进入 存活等待更新状态。

pop返回

我们在第三页(third)页面,点击back调用pop方法返回到第二页(second),此时的输出为:

1
2
3
4
5
6
second componentWillReceiveProps is here
second shouldComponentUpdate is here
second componentWillUpdate is here
second render is here
third componentWillUnmount is here
second componentDidUpdate is here

也就是说,点击back按钮之后,第二页(second)会进入更新组件流程,重新渲染界面,渲染之后,第三页(third)组件被卸载,最后是第二页更新完毕。

此时组件存活情况:

  • index组件:存活,等待更新。
  • first组件:存活,等待更新。
  • second组件:又更新一次,进入 存活等待更新状态。
  • third组件:被卸载,等待初始化。

如果再次pop,second组件也会被卸载,first组件会更新组件重新渲染。
此时的组件存活情况回到了first刚刚加载的情况。

结论

通过一番简单的测试后,我们可以得出关于navigator以及组件生命周期的几个结论:

  1. 首先初始化,navigator会保持一个类似“组件栈”的东西,里面保存了存活页面组件(此时是第一页)。栈的原理是后进先出,先push进来的,最后才会被pop出去。
  2. push方法会让当前页进入更新组件流程,在render的时候加载下一页组件,加载之后,当前页组件和下一页组件都存活。navigator组件栈添加这个“下一页”组件。
  3. pop方法,首先是让navigator看看自己的组件栈里面有没有“前一页”这个东西,如果有,让前一页进入更新组件流程,在render后卸载当前页组件,更新完毕后,前一页存活,当前页死掉。navigator组件栈里面去掉“当前页”组件。当然,如果组件栈里面就只有一页,那就啥都不干。
  4. 存活的组件的内容一直存在,如果你在push到下一页之前,通过setState或别的办法刷新了当前页,修改了当前页数据(比如我demo里面有个changeState的按钮会修改state.id)。然后push到下一页,再pop回来,这个修改了数据的状态还是存在的。
  5. 刚才那个保留状态仅针对pop方法,如果你是第一页修改数据,然后push到第二页,然后在第二页push回第一页。此时你看到的“第一页”是个新的组件,会走初始化加载组件的步骤。这个时候的navigator组件栈是这样的:第一页(修了数据)→第二页→第一页(新)。一直push的
  6. 只用push会导致中组件栈越来越大,没有任何好处,所以尽量避免导致无限push的情况发生。