ReactNative整理:《ReactNative系列》
前言
前面两篇文章我们已经介绍了RN的基本知识和生命周期,这篇讲讲对RN中常用常见知识的理解,想看前面基础文章的可以点下面的链接去看。
我们可以将这些关键字两两分为一组,state
和props
;let
和const
;Promise
机制中的resolve
和reject
、then
和catch
;async
和await
相互类比起来更容易理解。
ReactNative开发(一):简介及环境搭建
ReactNative开发(二):组件生命周期详解
一、状态与属性(state
、props
)
官方文档的说明:
props:组件在创建时可以用参数来定制。用于定制的参数就称为props
。
state:props是在父组件中指定,而且一经指定,在被指定的组件生命周期中不再改变。对于需要改变的数据,我们需要用state
。
通俗来讲,props
和state
是控制一个组件的两种数据。属性props
是组件生成时所带的参数,组件自身内部不可改变;如果想要修改,只能通过外部修改参数达到效果。状态state
是组件自身私有的,是没有办法从外部传入组件内部的。React把组件看成是状态机,会监听组件中state的变化,只要state发生了变化就会引起组件的重新render,更新DOM。我们可以做个简单Demo帮助理解:
1、父组件:
import React, { Component } from 'react';
import {StyleSheet, Text, View, TouchableOpacity } from 'react-native';
// 父组件
export default class Parent extends Component {
constructor(props) {
super(props);
this.state = {
parent: '父组件初始值'
};
this.handleParentPress = this.handleParentPress.bind(this);
}
componentWillMount() {
console.log('-parent-WillMount-');
}
componentDidMount() {
console.log('-parent-DidMount-');
}
componentWillReceiveProps() {
console.log('-parent-WillReceiveProps-');
}
componentWillUpdate() {
console.log('-parent-WillUpdate-');
}
componentDidUpdate() {
console.log('-parent-DidUpdate-');
}
/** 按钮点击处理 **/
handleParentPress() {
this.setState({ parent: '父组件改变' });
}
render() {
console.log('-parent-render-');
return (
<View style={styles.parentContainer}>
<TouchableOpacity onPress={this.handleParentPress}>
<Text style={styles.contentText}>
{this.state.parent}
</Text>
</TouchableOpacity>
</View>
);
}
}
在这里把组件生命周期方法也加了进来以便理解,通过log可以看到周期方法的运行顺序。在这里强调一下:如果组件render函数中存在对state
的使用,必须要在构造函数中声明this.state = {}
,否则运行会报错;如果没有使用state
,不会影响运行。
页面运行结果如下:
按钮点击后更新state:
可以看到两次state
不同,由开始的“父组件初始值”修改成了“父组件改变”,页面刷新后文字也发生了改变。生命周期方法少了shouldComponentUpdate
,建议最好不要随意重写这个方法,除非很明确页面刷新的判断条件,否则容易导致state或者props改变之后页面也不刷新;当然,这个方法是应用性能优化时的一个优化点。如果重写,可以返回默认值return true
。
2、子组件:
class Child extends Component {
constructor(props) {
super(props);
this.state = {
text: props.childContent
};
this.handleChildPress = this.handleChildPress.bind(this);
}
componentWillMount() {
console.log('-child-WillMount-');
}
componentDidMount() {
console.log('-child-DidMount-');
}
componentWillReceiveProps(nextProps) {
console.log('-child-WillReceiveProps-');
console.log(this.props, '-this-');
console.log(nextProps, '--next--');
this.setState({ text: nextProps.childContent });
}
componentWillUpdate() {
console.log('-child-WillUpdate-');
}
componentDidUpdate() {
console.log('-child-DidUpdate-');
}
handleChildPress() {
console.log('-child-handleChildPress-');
this.setState({ text: '子组件变了'});
}
render() {
console.log('-child-render-');
console.log(this.props, '==child-props==');
return (
<View style={styles.childContainer}>
<TouchableOpacity onPress={this.handleChildPress}>
<Text style={styles.contentText}>
{this.state.text}
</Text>
</TouchableOpacity>
</View>
);
}
}
在父组件中添加一条state数据,用来控制子组件属性值,并在点击父组件时,修改这条state数据:
// 父组件中构造函数
this.state = {
parent: '父组件初始值',
childContent: '子组件初始值'
};
// 父组件点击事件回调方法
handleParentPress() {
console.log('-parent-handleParentPress-');
this.setState({
parent: '父组件改变',
childContent: '子组件改变'
});
}
另外需要调整父组件的render函数,在其中加入子组件,并传入属性值childContent
。
// 父组件render函数
render() {
console.log('-parent-render-');
console.log(this.state, '-parent--state-');
return (
<View style={styles.parentContainer}>
<TouchableOpacity onPress={this.handleParentPress}>
<Text style={styles.contentText}>
{this.state.parent}
</Text>
</TouchableOpacity>
<Child childContent={this.state.childContent}/>
</View>
);
}
注意:组件的构造方法中存在props
,此时的props就是该组件的属性值。所以,我们可以通过构造函数中的props,将子组件的属性值childContent,当做其state的初始值this.state = { text: props.childContent }
用作页面展示。
上面是运行效果图:
注意两个箭头所指的日志,第一个是点击父组件按钮,第二个是点击子组件按钮;
(1)未点击父组件前:可以看到调用生命周期方法时,先调用父组件的componentWillMount再调用子组件的;而componentDidMount则是先调用子的,再调用父的。因为渲染入口是父组件,子组件是挂到父组件的render函数中的,所以会先调用父组件的componentWillMount和render,但是componentDidMount是渲染完成时的回调,只有当父组件中包含的所有子组件渲染完毕,父组件才算是渲染完毕,因此子组件调用componentDidMount方法在父组件之前。
(2)点击父组件按钮后:更新了state值,传入子组件的属性值发生了改变,所以在子组件中调用了componentWillReceiveProps方法,
this.props
和nextProps
分别代表初始属性值和修改过后的属性值;在该方法中修改子组件的state值为修改后的属性值,可以看到子组件的显示值变化,而且没有重复调用子组件的render函数。(3)点击子组件按钮后:更新了子组件的state值,只调用了子组件自身的周期函数,并没有影响到父组件,不会引起父组件的渲染。
可以利用拆分子组件的方式减少全局渲染频率,提高性能。
总之,
state
和props
的深入理解和灵活使用会使页面体验效果更好,数据处理更便捷;同时也是性能优化点,尽量减少页面状态的刷新能降低页面渲染资源开销,提升应用性能。
二、变量声明(let
、const
)
简述:let
和const
是ES6中新加入的命令,在ES5中用的是var
;它们的区别主要在于:var
定义的变量可以变量提升,没有块的概念;let
和const
定义的变量只能在块作用域内访问。let
定义的是变量,const
定义的是常量,需要初始化赋值。const
定义基本类型时不可修改,比如数字、字符串;定义复合类型时可修改其中的值,比如数组。
Q:什么是变量提升?
A:是JS语法中的概念,ES6之前是没有块作用域,只有全局作用域及函数作用域,变量提升是将变量声明提升至所在作用域的最开始部分。
下面用代码帮助理解:
-
let
用法
function test() {
console.log(name, '-name-');
// 输出 => undefined "-name-"
let name;
name = '姓名初始值';
console.log(name, '==name11==');
// 输出 => 姓名初始值==name11==
name = '姓名赋值成功';
console.log(name, '==name22==');
// 输出 => 姓名赋值成功 ==name22==
}
let
声明变量,可以不用赋初始值;在变量的作用域内,可以进行修改;不能在同一作用域内重复声明;在变量声明前是不可用的。
-
const
用法
function test() {
console.log(name, '-name-');
// 输出 => undefined "-name-"
const name = 'zhang';
const array = [];
// name = 'qwert';
// array = [0, 1, 2];
// 提示Attempt to assign to const or readonly variable
console.log(array, '==array11==');
// 输出 => [] "==array11=="
array[0] = 0;
array[1] = 1;
array[2] = 3;
console.log(array, '==array22==');
// 输出 => (3) [0, 1, 3] "==array22=="
}
const
声明常量,必须赋初始值,并且基本类型赋值后不可改变。与let
类似,不能在同一作用域重复声明,在声明前不可用。
注意:在声明数组、map这类复合变量时,是可以其修改内容的。上面例中我们可以看到,定义数组array赋空值,如果用array = [1, 2, 3]
的方式是不可以修改数组的,也不合语法;而用array[i] = 值
的方式却能修改其内容。这是因为第一种赋值方式是相当于将数组的地址赋给变量array,定义的数组地址不可修改,所以不成功;而第二种赋值方式是在不修改变量地址的前提下为数组修改内容,所以可以生效。
-
var
用法
{
var name = 'qwert';
var name = '1111';
}
// 输出 => 1111 ==name==
console.log(name, '==name==');
(function abcd() {
var age = 15;
console.log(age, '==age==');
})();
// console.log(age, '==age00=='); 报错
可以看到var
声明的变量可以跨块访问,但是不能跨函数访问;而且在同一块内可以重复声明。正因为可以跨块访问,同时可以重复声明,所以有可能会引起不同块区域中变量的相互影响。
对比let
、const
和var
可以发现:前两者类似Java中的变量声明方式,都是先声明后使用,不可重复声明;但是var
可以跨块访问,有可能引起变量值的覆盖或混乱,所以使用时需要特别注意。
三、Promise机制
- Promise简介
Promise
是ES6中新增的编程方式,是异步编程的一种解决方案;它可以在异步操作中灵活的处理错误;支持链式调用;从字面含义来看表示承诺,承诺过一段时间会返回一个结果,同时它也是个对象,从中可以获取到异步操作的信息。 - 三种状态
Promise
有三种状态,分别是:
1.pending
-- 等待状态;
2.fullfield
(或resolved
) -- 成功状态;
3.rejected
-- 失败状态。
三种状态的转换途径只有两种,初始状态是pending
:
1. 任务完成:pending
-->resolved
(等待 --> 完成);
2. 任务失败:pending
-->rejected
(等待 --> 失败); - Promise的方法
Promise中方法可以用console.dir(Promise)
打印出来观察:包含all
、race
、reject
、resolve
方法,同时prototype中还有我们常见的then
、catch
、finally
方法。
- Promise创建
let promise = new Promise((resolve, reject) => {});
JS中提供了构造函数去创建Promise
对象,需要传的参数是用户自定义的方法,用来处理异步任务;其中resolve
和reject
是JS提供的回调函数,不需要自己定义实现。异步操作成功,就调用resolve
,将结果数据传入resolve
,Promise从pending
状态变成resolve
;异步操作失败,则调用reject
,需要将错误对象当参数传进去,此时,Promise从pending
状态变成reject
。
-
then
方法
但凡异步操作,一般都是需要回调函数来处理接下来的业务的,而then
方法就是将原来的回调函数抽离出来,在异步操作结束后,通过链式调用的方式执行回调;then
方法强大的地方在于它可以在内部继续写Promise对象,并返回;也可以直接return
数据而不是Promise
对象。
then
方法可以有两个参数,一个是成功resolve
的回调,第二个是失败reject
的回调方法。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('-start-');
// 取随机数 * 10
let random = Math.random() * 10;
// 向下取整
let count = Math.floor(random);
console.log(count, '-count-');
if (count < 5) { // 整数小于5判定为成功
resolve('--任务成功--');
} else { // 整数大于等于5判定为失败
reject('==任务失败==');
}
}, 1000);
});
promise.then(success => {
console.log(success, '--success--');
return '成功之后返回';
}, failed => {
console.log(failed, '--failed--')
}).then(data => {
console.log(data, '=data=')
});
结果输出日志:
成功日志:
失败日志:
-
catch
方法
catch
方法的作用主要有两个:一是用来指定reject
的回调;二是捕捉then
方法中的异常,避免程序报错卡死。
修改上面代码中的Promise调用方法:
promise.then(data => {
console.log(data, '=then=');
console.log(aaa);
}).catch(error => {
console.log(error, '=error=')
});
异步操作成功时,会链式执行then
方法,但是其中aaa
是未声明的变量,所以会报错:
异步操作失败时,会调用
reject
,catch
方法中接收到抛出的错误日志并打印:-
all
方法
all
方法为Promise提供了并行异步操作的能力。简单来说,即可以同时执行多个异步操作,all
方法所需要的参数是以需要执行的异步操作为元素的数组。并且在所有异步任务执行完毕后才执行回调操作。等到都执行完后,会回调到then
方法中,data就是最终的返回结果。
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('=执行1=');
resolve('--任务1--');
}, 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('=执行2=');
resolve('--任务2--');
}, 1000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('=执行3=');
resolve('--任务3--');
}, 1000);
});
Promise.all([promise1, promise2, promise3]).then(data => {
console.log(data, '--执行结果--');
}).catch(error => {
console.log(error, '--error--')
})
注意: 返回的结果也是数组,结果元素的顺序和传入异步参数的顺序一一对应;异步任务只要有任一个失败都会回调catch方法。
-
race
方法
race
字面意思是赛跑,所以强调的是以速度最快的为准执行回调。race
的用法和all
类似,参数都是以异步操作为元素的数组,不同的是:all
是所有异步操作执行完后才会回调then
方法,以最慢的为准;而race
是只要有一个执行完就立刻回调then
,以最快的为准。
将上面代码中的all
改为race
,并且调整后两个异步操作的执行延迟时间为1500,时间相同的话表现不出来前后差异:
Promise.all([promise1, promise2, promise3]).then(data => {
console.log(data, '--执行结果--');
}).catch(error => {
console.log(error, '--error--')
})
注意:第一个异步操作执行完后回调then
后,其他未执行完的异步操作会继续执行,不会中断停止。
Promise
常用的知识和方法这里就介绍完了,更深入的理解可以结合具体的项目来研究。
四、异步与等待(async
和await
)
-
async
是异步的意思。用来标识函数为异步函数,往往函数中有耗时操作时会用到async
,而异步函数在执行时不会阻塞后面代码运行。 -
await
是等待的意思。必须在async
修饰的函数中使用,用来标识耗时操作的语句;当程序执行到await
所在行时,会阻塞并等待,直到异步任务得到返回结果。
代码验证:
componentDidMount() {
console.log(new Date().toTimeString(), '==start==');
this.getImageData().then(data => {
console.log(data, new Date().toTimeString() + ' -data-');
});
console.log(new Date().toTimeString(), '==end==');
}
async getImageData() {
try {
let response = await fetch('http://www.pptbz.com/pptpic/UploadFiles_6909/201309/2013093019370302.jpg');
console.log(response, new Date().toTimeString() + ' --response--');
let path = response.url;
this.setState({ image: path });
return path;
} catch (error) {
console.log(error, '-error-');
}
}
输出日志:
可以看到:从打印时间上来看,start日志和end日志基本没有时间差,说明用
async
修饰的getImageData
方法没有阻塞end日志的输出;而response日志输出时间要比end日志晚5秒左右,说明用await
修饰的fetch
语句在做耗时操作,并且一直在等待,直到拿到返回值,输出日志;async
函数返回的是Promise
,因此可以在then
方法中得到返回值。
结尾
以上介绍的技术点都是ReactNative中常见常用的,是个人回顾整理的记录,分享出来希望更多想学RN的同学看到。仅供参考,如果看到有问题或错处,欢迎指出,互相交流!!