赞
踩
React Native
(以下简称 RN
)是 Facebook 研发并开源的应用框架,由 React
和应用平台的原生功能组成。RN
的主要编程语言是 JavaScript
(以下简称 JS
),所以可以使用后端强⼤的 Web 方式管理,做到既能⾼效开发,又可以实现快速部署和热修复。
容易上手: RN
入门较轻松,只要有一门面向对象语言的基础,都可以很快上手,而且社区较活跃,很多资料在网上都能查到。
垮平台开发 :相比原生的ios 和 android app各自维护一套业务逻辑大同小异的代码,React Native 只需要同一套javascript 代码就可以运行于ios 和 android 两个平台,在开发、测试和维护的成本上要低很多。
快速编译: 相比原生代码需要较长时间的编译,React Native 采用热加载的即时编译方式,使得App UI的开发体验得到改善,几乎做到了和网页开发一样随时更改,随时可见的效果。而且很稳定,基本不会有那种热更新几次后,界面卡死然后重新跑的情况出现。
快速发布:RN
可以通过 JSBundle
即时更新 App。相比原来冗长的审核和上传过程,发布和测试新功能的效率大幅提高。
和 React for the Web
类似,RN 也是使用 JSX
语法开发的。JSX
可以简单的理解为 JavaScript + XML 的语法糖,是 JS 的扩展语法,在编译的时候会被转换成标准的 JS 语法,如下:
// 编译前 export default class Sample extends Component { render() { return ( // JSX <View style={styles.container}> <Text style={styles.title}> Welcome to React Native! </Text> <View /> <Text>测试</Text> </View> ); } } // 编译后 - 这里只展示 render 方法里面的简化后的编译情况,要看完整的编译代码可以点击下面的链接去尝试编译,不过差别也不是很大。 export default class Sample extends Component { render() { return ( React.createElement( View, { style: styles.container }, React.createElement( Text, { style: styles.title }, "Welcome to React Native!" ), React.createElement(View, null), React.createElement(Text, null, "\u6D4B\u8BD5") ) ); } }
可以看到,编译过后的 JSX
标签都被转换成了 React.createElement(component, props, ...children)
函数的语法糖,所以 即使不用 JSX
,只用纯 JS 也是可以进行页面开发的,但从上面的代码来看,JSX
的表达比纯 JS 代码的表达更清晰一些,能更好的看出 视图的结构。而且 JSX
是在打包的过程中才被编译成 JS 代码,因此不用担心会影响到性能。
如果想试试其他的 JSX 或 Class 编译后的样子,可以尝试使用在线Babel编译器。
RN
的底层引擎是 JS
内核,但渲染时用的是原⽣的组件⽽非 HTML5
的组件,因此只需要使用 JS
就可以编写一个原生移动应用,并且在运⾏时做到与 Navive App 相近的性能体验,在正常的使用感受上,几乎无法区分这个应用是由原生语言编写的还是由 JS
编写的。以下图片可以简要说明,RN
组件和原生组件的对应关系:
转换成代码大概是这样的:
render() {
return (
<View style={{ flexDirection: 'row'}}>
<Image />
<Text>Pouncival</Text>
</View>
);
}
RN | iOS | Android | 概述 |
---|---|---|---|
<View> | UIView | ViewGroup | 创建 UI 时最基础的组件,View 是一个支持 Flexbox 布局、样式、一些触摸处理、和一些无障碍功能的容器,并且它可以放到其它的视图里,也可以有任意多个任意类型的子视图。 |
<Text> | UITextView | TextView | 显示文本的组件,并且它也支持嵌套、样式,以及触摸处理。 |
<Image> | UIImageView | ImageView | 显示多种不同类型图片的组件,包括网络图片、静态资源、临时的本地图片、以及本地磁盘上的图片(如相册)等。 |
<TextInput> | UITextField | EditText | 允许用户在应用中通过键盘输入文本的基本组件。该组件的属性提供了多种特性的配置,譬如自动完成、自动大小写、占位文字,以及多种不同的键盘类型(如纯数字键盘)等等。 |
<ScrollView> | UIScrollView | ScrollView | 一个封装了平台的 ScrollView(滚动视图)的组件,同时还集成了触摸锁定的“响应者”系统。ScrollView 必须有一个确定的高度才能正常工作,因为它实际上所做的就是将一系列不确定高度的子组件装进一个确定高度的容器(通过滚动操作)。 |
<FlatList> | UIScrollView | ScrollView | 高性能的简单列表组件,FlatList 会惰性渲染子元素,只在它们将要出现在屏幕中时开始渲染。这种惰性渲染逻辑要复杂很多,因而 API 在使用上也更为繁琐。除非你要渲染的数据特别少,否则你都应该尽量使用FlatList ,哪怕它们用起来更麻烦。 |
ScrollView
和FlatList
应该如何选择?ScrollView 会简单粗暴地把所有子元素一次性全部渲染出来。其原理浅显易懂,使用上自然也最简单。然而这样简单的渲染逻辑自然带来了性能上的不足。想象一下你有一个特别长的列表需要显示,可能有好几屏的高度。创建和渲染那些屏幕以外的 JS 组件和原生视图,显然对于渲染性能和内存占用都是一种极大的拖累和浪费。FlatList 使用 tips
- 给
keyExtractor
属性赋值,给 item 指定唯一的 Key,也可以给 item 的key
属性赋值,像这样<Item key={item.id} />
。这么做是为了区分同类元素的不同个体,以便在刷新的时候能确定变化的 item,就只更新该 item,其他复用之前的,减少重新渲染的开销。如果不给FlatList
的keyExtractor
属性赋值,RN 会默认使用item.key
作为 Key 值,如果item.key
也不存在,则会使用数组下标(使用数组下标可能会导致组件出现数据错乱的问题)。使用方法如下:// 属性声明 (item: object, index: number) => string; // 使用 keyExtractor={(item, index) => item.id}
- 1
- 2
- 3
- 4
- 5
- 如果列表的行高是固定的,建议给
getItemLayout
属性赋值,可以避免动态测量内容尺寸的开销,对于元素较多的列表,可以极大的提高性能。需要注意的是,如果指定了分割线ItemSeparatorComponent
,则需要把分割线的尺寸也考虑到offset
的计算中,getItemLayout
的使用方法如下:// 属性声明 (data, index) => { length: number, offset: number, index: number}; // 使用 getItemLayout={(data, index) => ( {length: item_height, offset: height * index, index} )}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
RN 中的视图布局使用的是一种实现了Flexbox
规范且基于CSS
的跨平台布局系统。该系统可以在不同的屏幕尺寸上提供一致的布局结构。
随着这个系统的不断完善,Facebook决定对它进行重启发布,并取名
Yoga
。Yoga
是一个表达性极强的布局库,并没有实现CSS
的所有内容。没有支持表、浮动等某些类似的CSS
概念。Yoga
也不支持对于布局无影响的部分、比如颜色和一些背景属性。Yoga
是基于C
实现的。之所以选择C
,首先当然是从性能方面考虑的。基于C实现的Yoga
比之前Java实现在性能上提升了33%。其次,使用C实现可以更容易地跟其它平台集成。如果对Yoga
感性去,可以到官方文档去仔细研究
一般来说,使用flexDirection
、alignItems
和 justifyContent
三个样式属性就已经能满足大多数布局需求。
FlexBox
的全称是Flexible Box
,意为弹性布局,用于为盒状模型提供最大的的灵活性。是 Web 端的一种布局方案,RN 中的 Flexbox 的工作原理和 Web 上的 CSS 基本一致,但是也存在少许差异。首先是默认值不同:flexDirection
的默认值是column
而不是row
,而flex
也只能指定一个数字值,并且有一些属性在 RN 里面也是没有的,比如 Flex-Flow
。下面是一个 flex 容器的图解:
flex
flex
属性决定元素在主轴上如何填满可用区域。整个区域会根据每个元素设置的 flex 属性值被分割成多个部分。在下面的例子中,在设置了flex: 1
的容器 view 中,有红色,黄色和绿色三个子 view。红色 view 设置了flex: 1
,黄色 view 设置了flex: 2
,绿色 view 设置了flex: 3
。1+2+3 = 6,这意味着红色 view 占据整个区域的1/6
,黄色 view 占据整个区域的2/6
,绿色 view 占据整个区域的3/6
。
import React from 'react';
import { View } from 'react-native';
export default class AlignItemsBasics extends React.PureComponent {
render() {
return (
<View style={{flex: 1}}>
<View style={{ flex: 1, backgroundColor: 'red'}} />
<View style={{ flex: 2, backgroundColor: 'yellow'}} />
<View style={{ flex: 3, backgroundColor: 'green'}} />
</View>
);
}
};
flexDirection
在组件的style
中指定flexDirection
可以决定布局的主轴方向。即决定子元素是沿着**水平轴(row
)方向排列,还是沿着竖直轴(column
)方向排列,默认值是竖直轴(column
)**方向。下图是flexDirection
的概括:
justifyContent
在组件的 style 中指定justifyContent
可以决定其子元素沿着主轴的排列方式。
alignItems
在组件的 style 中指定alignItems
可以决定其子元素沿着次轴(与主轴垂直的轴,比如若主轴方向为row
,则次轴方向为column
)的排列方式。
在RN中给组件传递属性很简单,只需要在组件标签上添加属性名和相应的值,然后在组件中使用this.props
获取相应的属性,这就完成了一次数据的传递。
<CustomComponent name="test" />
// 在 CustomComponent 里使用 this.props 获取
this.props.name
如果想要给组件的props
添加初始值,则可以使用defaultProps
类属性,如下:
class CustomComponent extens React.Componet {
static defaultProps {
name: 'defaultName'
}
}
// 也可以这样
CustomComponent.defaultProps = {
name: 'defaultName'
}
还有就是如果对某个组件的属性有类型的要求,也可以使用特定的propTypes
属性来进行类型检查,用法和defaultProps
一样,如下:
import PropTypes from 'prop-types';
class CustomComponent extens React.Componet {
static propTypes {
age: PropTypes.number
name: PropTypes.string
}
}
// 也可以这样
CustomComponent.propTypes = {
age: PropTypes.number
name: PropTypes.string
}
state
是一个普通 JavaScript 对象,可以由用户自定义(只能在构造函数里面自定义)。state
与props
类似,但state
是私有的,并且完全受控于当前组件,所以组件的数据更新是通过state
对象来进行的,要更新组件的数据需要调用this.setState()
方法,如下:
import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export default class TimerDemo extends React.Component { constructor(props) { super(props); this.state = { date: new Date() } } componentDidMount() { this.timerId = setInterval(() => this.udpateCurrentTime(), 1000); } componentWillUnmount() { clearInterval(this.timerId) } render() { let { currentTime } = this.state; currentTime = currentTime || new Date(); const currentTimeDesc = currentTime.toLocaleTimeString(); return( <View style={styles.rootContainer}> <Text style={styles.timeText}>{`当前时间 ${currentTimeDesc}`}</Text> </View> ); } udpateCurrentTime() { this.setState({ date: new Date() }) } } const styles = StyleSheet.create({ rootContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, timeText: { fontSize: 30 }, })
不要直接修改state
,例如:
this.state.date = new Date()
// 上面的代码并不会重新渲染组件,应该要这样更新
this.setState({ date: new Date() })
state
的更新可能是异步的,所以如果下一次数据更新依赖上一个state
的更新值的话,可能会导致结果不准确。例如:
// 计数器的更新
this.setState({
counter: this.state.counter + this.props.increment,
});
// 要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
state
的更新会被合并,因为RN会延迟调用setState()
,然后将多个setState()
合成一个调用。
TouchableHighlight: 该组件用于封装视图,使其可以正确响应触摸操作。当按下的时候,封装的视图的不透明度会降低,同时会有一个底层的颜色透过而被用户看到,使得视图变暗或变亮。
TouchableHighlight
的底层实现会创建一个新的视图到视图层级中,如果使用的方法不正确,有时候会导致一些不希望出现的视觉效果。譬如没有给视图的 backgroundColor 显式声明一个不透明的颜色。
TouchableOpacity: 该组件用于封装视图,使其可以正确响应触摸操作。当按下的时候,封装的视图的不透明度会降低。此组件与 TouchableHighlight 的区别在于并没有额外的颜色变化,更适于一般场景。
TouchableOpacity
的不透明度的变化是通过把子元素封装在一个Animated.View
中来实现的,这个动画视图会被添加到视图层级中,少数情况下有可能会影响到布局。
TouchableWithoutFeedback: 该组件用于封装视图,使其可以正确响应触摸操作。但是在按下后没有任何的视觉反馈,所以官方并不建议使用该组件,但可根据需求使用,例如想实现点击空白处触发某个操作,就可以使用该组件。
TouchableNativeFeedback: 该组件用于封装视图,使其可以正确响应触摸操作,但是仅限于 Android 平台,因为这是为了支持Android5.0
新增的触控反馈效果而设计的。在 Android 设备上,这个组件利用原生状态来渲染触摸的反馈(水波纹效果)。
TouchableNativeFeedback
在底层实现上,实际会创建一个新的 RCTView 节点替换当前的子 View,并附带一些额外的属性。
使用Touchable
系列组件封装组件后,给 onPress
赋值一个回调方法即可获取点击事件。代码如下
import React, { useState } from "react"; import { StyleSheet, Text, TouchableOpacity, TouchableHighlight, View } from "react-native"; const App = () => { const [count, setCount] = useState(0); const onPress = () => setCount((prevCount) => prevCount + 1); return ( <View style={styles.container}> <View style={styles.countContainer}> <Text>Count: {count}</Text> </View> <TouchableHighlight style={styles.button} onPress={onPress} underlayColor="grey" > <Text>Press Here</Text> </TouchableHighlight> <TouchableOpacity style={[styles.button, { marginTop: 10 }]} onPress={onPress} > <Text>Press Here2</Text> </TouchableOpacity> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: "center", paddingHorizontal: 10 }, button: { alignItems: "center", backgroundColor: "#DDDDDD", padding: 10 }, countContainer: { alignItems: "center", padding: 10 } }); export default App;
以上的组件仅
TouchableOpacity
和TouchableHighlight
支持Style
样式属性,另外两个都不支持。并且Touchable
系列组件只支持一个子节点(不能没有子节点也不能多于一个)。如果你希望包含多个子组件,可以用一个 View 来包装它们。
每个组件都包含 生命周期方法,这些方法都可以被重写,在运行过程中特定的阶段会调用这些方法。你可以使用此以下图表作为速查表。在下述说明中,常用的生命周期方法会被加粗,没加粗的则表示使用较少的生命周期函数。
RN
的生命周期可以大致分为4个阶段:
Mounting :挂载阶段,主要是创建组件实例并将其插入到 DOM
树中。方法调用顺序如下:
constructor():
static getDerivedStateFromProps()
render()
componentDidMount()
Updating:更新阶段,主要是通过接受新的参数来更新组件。方法调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
Unmounting:卸载阶段,从实际的 DOM
树中删除一个组件。方法调用顺序如下:
componentWillUnmount()
Error Handing:错误处理的阶段,在 Render
期间、生命周期的方法、子组件的构造方法中出现错误时,会进入该阶段。方法调用顺序如下:
static getDerivedStateFromError()
componentDidCatch()
生命周期方法的详细说明可以在官方文档查看。
RN中有4个线程:
UI线程: 用于iOS
和Android
原生组件的呈现和交互,也是应用的主线程。
JS线程: 这是RN逻辑运行的线程,比如执行JS
代码、进行相关的API调用、处理触摸事件和其他各种操作。
更新视图的操作通常是在
JS线程
每个事件循环结束时,批量发送到原生端,并在UI线程
执行这些更新操作。所以避免掉帧卡顿的情况出现,JS线程
应该在下一帧渲染之前向UI线程
发送批量更新操作,因此尽量不要在更新UI的操作里面插入一些复杂的运算,这会导致界面卡顿,可以把复杂的操作放到InteractionManager.runAfterInteractions(() => {})
里面,该方法的回调会在动画和交互结束的时候调用。。
ridge线程: 用于处理RN和原生之间的通信。
Render线程: 只有 Android L(5.0) 之后才有,用于生成绘制 UI 的 OpenGL 命令。
从以上介绍可得知,RN的代码只有一个线程在运行,所以为了避免某一个任务耗时很长,后面的任务排队等着,导致整个拖延执行的情况出现,JS将任务的执行模式分成了两种,同步和异步。同步模式
就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;异步模式
则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
使用回调函数实现异步编程的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,比如想要顺序执行异步代码,就会出现回调地狱
,比如下面的代码:
a(function(resultsFromA) {
b(resultsFromA, function(resultsFromB) {
c(resultsFromB, function(resultsFromC) {
d(resultsFromC, function(resultsFromD) {
e(resultsFromD, function(resultsFromE) {
f(resultsFromE, function(resultsFromF) {
console.log(resultsFromF);
});
});
});
});
});
});
因为回调函数的可读性非常差,代码光是看都要看半天,中间如果一走神,又要从头开始看,这就让人很难受,所以就出现了更干净,更线性,可读性更好的Promise
。
Promise
是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,通过它可以获取异步操作的消息。Promise 提供统一的 API,所以各种异步操作都可以用同样的方法进行处理。
Promise
对象有以下两个特点。
对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise
的优点:
Promise
对象提供统一的接口,使得控制异步操作更加容易。Promise
的缺点:
Promise
,一旦新建它就会立即执行,无法中途取消。Promise
内部抛出的错误,不会反应到外部。pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。如果某些事件不断地反复发生,一般来说,使用
Stream
模式是比部署Promise
更好的选择。
Promise
是一个对象,所以可以用构造函数来生成Promise
实例,代码如下:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;而reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
有了Promise
,可以使用链式调用的形式轻松实现异步代码顺序执行,例如,我们需要获取登录用户的信息,然后获取用户的好友列表,最后获取该用户每个好友的信息,等所有好友信息获取完成后,将它们打印出出来。
如果使用单纯的回调函数来实现就会很困难和麻烦,但是有了Promise
,我们可以链式调用的形式来执行这些流程,当获取到好友列表后,使用 map 函数将每个 ID 生成一个Promise
,该Promise
用于获取相对应好友的信息。最后,我们可以调用 Promises.all ()
等待数组中的所有Promise
完成后才返回最终结果,然后将它打印出来。代码如下:
fetchJSON('/user-profile') .then((user) => { return fetchJSON(`/users/${user.id}/friends`); }) .then((friendIDs) => { let promises = friendIDs.map((id) => { return fetchJSON(`users/${id};`); }); return Promise.all(promises); }) .then((friends) => console.log(friends)) .catch((error) => { console.log(error); }); /* Promise.catch() 方法是 Promise.then(null, rejection) 或 Promise.then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。 fetchJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。 在 Promise 的调用链中,不需要每个 Promise 都指定 Promise.catch() 方法,因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。所以只需要在调用链的末尾指定一个 Promise.catch() 方法即可。 */
如果是单纯的回调函数实现,则为:
fetchJSON('/user-profile', (user) => { fetchJSON(`/users/${user.id}/friends`, (friends) => { handleResult(friends, (friendIDs) => { let completeCount = 0 let hasError = false let friendsInfo = [] friendIDs.map((id) => { return fetchJSON(`users/${id};`, (result, hasError) => { completeCount += 1 hasError = hasError friendsInfo.add(result) }); }); while(completeCount < friendIDs.lenght && !hasError) { // ..循环等待所有好友信息被成功获取 } console.log(friendsInfo); }); }); });
代码量更多不说,可读性也很差。
与单纯的回调函数相比,Promise
更加的容易实现和维护,但从上面的代码来看,虽然Promise
比单纯的回调函数可读性更好,但依然需要依赖回调来实现异步函数的顺序调用,随着调用链的增长,可读性也就变得越差。
所以为了避免这种情况,async/await
就出现了,async/await
是基于Promise
的语法糖,它可以让我们使用顺序编程风格的方式来按顺序执行我们的异步代码,使得异步代码更容易编写和阅读。
aysnc
关键字的使用很简单,只需要把它放在函数声明或箭头函数的见面就可以了,添加了async
关键字的函数也称作异步函数。而异步函数的特征之一就是函数的返回值为Promise
,代码如下:
function test1() {
return "async/await test 1"
};
let result = test1(); // 返回值为 "async/await test 1"
console.log(result)
let test2 = async function() {
return "async/await test 2"
};
// let test2 = async () => { return "async/await test 2" }
let promiseResult = test2(); // 返回值为 Promise
// 因为返回值为 Promise,所以可以使用 Promise.then() 通过回调获取值
// Promise 有个特性,就是状态一旦发生改变,就会凝固下来,保持同一个结果,即使添加新的回调函数,也会立即得到这个结果。
promiseResult.then( value => console.log(value) ) // 打印 "async/await test 2"
await
关键字需要和函数配套使用,因为它只能在异步函数内部工作,。执行到await
所在行时,程序会暂时退出异步函数,并在给定的任务完成时继续运行该函数。在此期间,程序可以运行其他的代码,比如手势响应和渲染方法。之前也说过,async/await
是Promise
的语法糖,所以await
只能用Promise
或者返回Promise
的函数的前面,但需要注意包含await
的函数在定义的时候要加上async
关键字。例如之前的代码可以改造成下面:
async function requestAllFriendInfo() { try { let user = await fetchJSON('/user-profile'); let friendIDs = await fetchJSON(`/users/${user.id}/friends`); let promises = friendIDs.map((id) => { return fetchJSON(`users/${id};`); }); let friends = await Promise.all(promises) console.log(friends) } catch(error) { console.log(error); } } // 还有一种捕获错误的方式,因为 async 函数总是会返回一个 Promise,所以可以像这样使用 requestAllFriendInfo().catch( error => console.log(error) );
和之前的Promise
调用链相比,代码更加简洁易懂。
虽然使用async/await
会让代码看起来像是同步执行的,但是执行到await
时,它所在的函数会被暂停,所以如果函数中有用到全局函数,它可能就会在函数暂停的时候在其他地方被改变,这有可能会导致函数执行的结果和期望的不一致。
await
会暂停函数,阻塞后面的代码,这意味着当await
变多时会导致代码执行变慢,因为每个await
都会等待前一个的完成,并不是所有请求都相互依赖,需要按顺序执行的。所以为了缓解这个问题,我们可以将Promise
对象存储在变量中,这时Promise
就会开始执行,然后在使用await
来等待它们执行完毕。代码如下:
function timeoutPromise(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("done"); }, interval); }); }; async function timeTest() { const timeoutPromise1 = timeoutPromise(3000); const timeoutPromise2 = timeoutPromise(3000); const timeoutPromise3 = timeoutPromise(3000); await timeoutPromise1; await timeoutPromise2; await timeoutPromise3; } // 这样子就只需要一个 Promise 多一点的时间就可以执行三个 Promise 了
RN
内置了三种网络请求的方式fetch
、XMLHttpRequest
、WebSocket
,XMLHttpRequest(XHR)
和WebSocket
都是由两部分组成: “前端” 和 “后端”。前端负责与JavaScript
交互,后端负责在原生平台上转换JavaScript
发送过来的请求为原生系统自己的请求。而fetch
是基于XMLHttpRequest
的,所以RN
网络请求逻辑实际上分为两个部分:一个是JS
的运行环境,一个是原生平台的运行环境。你在JS层调用网络请求时,其实是经历了两个过程才到达真正的服务器端。
React Native 提供了和 web 标准一致的Fetch API,用于满足开发者访问网络的需求。如果你之前使用过XMLHttpRequest
(即俗称的 ajax)或是其他的网络 API,那么 Fetch 用起来将会相当容易上手。
要从任意地址获取内容的话,只需简单地将网址作为参数传递给fetch
方法即可:
fetch("https://mywebsite.com/mydata.json");
Fetch 还有可选的第二个参数,可以用来定制 HTTP 请求一些参数。你可以指定 header 参数,或是指定使用 POST 方法,又或是提交数据等等:
fetch("https://mywebsite.com/endpoint/", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
firstParam: "yourValue",
secondParam: "yourOtherValue",
}),
});
常用的’Content-Type’除了上面的’application/json’,还有传统的网页表单形式,示例如下:
fetch("https://mywebsite.com/endpoint/", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "key1=value1&key2=value2",
});
可以参考Fetch 请求文档来查看所有可用的参数。
注意:使用 Chrome 调试目前无法观测到 React Native 中的网络请求,你可以使用第三方的react-native-debugger或者抓包工具
Charles
、Fiddler
等来进行观测。
网络请求天然是一种异步操作,fetch
方法会返回一个Promise
,示例代码如下:
function getMoviesFromApiAsync() { return fetch("https://facebook.github.io/react-native/movies.json") .then((response) => response.json()) .then((responseJson) => { return responseJson.movies; }) .catch((error) => { console.error(error); }); } // 使用 async/await async function getMoviesFromApi() { try { // 注意这里的await语句,其所在的函数必须有async关键字声明 let response = await fetch("https://facebook.github.io/react-native/movies.json"); let responseJson = await response.json(); return responseJson.movies; } catch (error) { console.error(error); } }
因为RN
内置了XMLHttpRequest API(也就是俗称的 ajax),所以一些基于 XMLHttpRequest 封装的第三方库也可以使用,例如frisbee或是axios等。但注意不能使用 jQuery,因为 jQuery 中还使用了很多浏览器中才有而 RN 中没有的东西(所以也不是所有 web 中的 ajax 库都可以直接使用)。
const request = new XMLHttpRequest();
request.onreadystatechange = (e) => {
if (request.readyState !== 4) {
return;
}
if (request.status === 200) {
console.log("success", request.responseText);
} else {
console.warn("error");
}
};
request.open("GET", "https://mywebsite.com/endpoint/");
request.send();
XMLHttpRequest
的 “后端“ 在 iOS 上是NSURLSession
,而在 Android 上则是OKHTTP
。
React Native 还支持WebSocket,这种协议可以在单个 TCP 连接上提供全双工的通信信道。
const ws = new WebSocket("ws://host.com/path"); ws.onopen = () => { // connection opened ws.send("something"); // send a message }; ws.onmessage = (e) => { // a message was received console.log(e.data); }; ws.onerror = (e) => { // an error occurred console.log(e.message); }; ws.onclose = (e) => { // connection closed console.log(e.code, e.reason); };
WebSocket
的 “后端” 在 iOS 上是封装过后的NSStream
,而在 Android 上则是OKHTTP
。
RN 有一个内置的开发菜单,我们可以通过这个菜单来打开调试工具、热更新代码、还有加载 js bundle
包。
1. 模拟器运行
- iOS
- 按下**
Command
** +D
快捷键- 通过菜单栏触发摇一摇的动作,Device → shake (
Control
+Command
+Z
)- Android
- 按下
Command
+M
(windows 上可能是 F1 或者 F2)- 命令行中运行
adb shell input keyevent 82
来发送菜单键命令。2.真机运行
摇一摇就可以了
开发菜单如下图所示:
一般都是通过 RN 内置的开发菜单的 Debug
选项来打开调试工具,如果是离线包调试,则可以通过弹窗的形式打印日志、或者其他的工具进行调试。选择 Debug
后,默认是打开 Chrome
开发者工具,因为 RN 是用 js 来开发的,所以调试 RN 的流程基本和网页、小程序的流程一样。以下是 Chrome
开发者工具的页面:
橙色方框部分是调试工具的菜单栏,可以根据需要选择相应的菜单,比较常用的选项是 Console
、Source
,其他的几乎不怎么用到 。Console
主要用于日志、警告、错误的打印,还可以在这里打印当前上下文的变量值,以及执行 js 语句。 Source
主要用于断点调试,即当前截图显示的页面,左侧的红色方框则是项目的文件目录。
目前 Chrome
的开发者工具只能调试代码逻辑,不能看到 App 的用户界面和视图架构,不过可以使用 RN 自带的 UI 调试工具,就在开发菜单的 Show Inspector
选项里面,如下图:
选中底部的 Inspect
,点击你想查看的视图组件,就会出现该组件的布局信息。红色方框里表示的是视图的层级,橙色方框里的是当前视图的 css 代码,蓝色方框里的是当前组件的代码的位置,紫色方框里的是当前视图的约束。
根据官方文档的说明,Chrome
的开发者工具目前不能直接观测到 RN 中的网络请求,所以如果需要进行网络调试,可以使用更强大的调试工具 react-native-debugger,或者也可以使用抓包工具,比如 fiddler
、charles
来进行调试。
点击开发菜单的 Show Perf Monitor
就可以打开性能监控工具,可以通过上面的信息大致分析 RN 的性能问题。
RAM: 当前应用的内存。
JSC: JavaScriptCore
托管堆的大小,只在触发垃圾收集的时候更新。
Views: 总共有两个数字,上面的数字表示当前屏幕上显示的视图数量,下面的数字则表示已经进行过布局和绘图计算的视图总数(包括屏幕外以及能够被合并的视图,例如嵌套的文本)。这两个数字的差值越小,额外的布局和绘图计算就会越少,则当前页面性能就会越好。
UI: UI 线程的帧率
JS: RN JS 线程的帧率。对大多数 RN 应用来说,业务逻辑是运行在 JS 线程上的。这是 React 应用所在的线程,也是发生 API 调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果 JavaScript 线程有一帧没有及时响应,就被认为发生了一次丢帧。
下面的列表是各个模块的执行时间
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。