简要介绍

什么是 ReactNative?

React Native (以下简称 RN)是 Facebook 研发并开源的应用框架,由 React 和应用平台的原生功能组成。RN 的主要编程语言是 JavaScript(以下简称 JS),所以可以使用后端强⼤的 Web 方式管理,做到既能⾼效开发,又可以实现快速部署和热修复。

RN 的优缺点

优点:
  • 容易上手: RN 入门较轻松,只要有一门面向对象语言的基础,都可以很快上手,而且社区较活跃,很多资料在网上都能查到。
  • 垮平台开发 :相比原生的ios 和 android app各自维护一套业务逻辑大同小异的代码,React Native 只需要同一套javascript 代码就可以运行于ios 和 android 两个平台,在开发、测试和维护的成本上要低很多。
  • 快速编译: 相比原生代码需要较长时间的编译,React Native 采用热加载的即时编译方式,使得App UI的开发体验得到改善,几乎做到了和网页开发一样随时更改,随时可见的效果。而且很稳定,基本不会有那种热更新几次后,界面卡死然后重新跑的情况出现。
  • 快速发布RN 可以通过 JSBundle 即时更新 App。相比原来冗长的审核和上传过程,发布和测试新功能的效率大幅提高。
缺点:
  • 动画性能: RN 在动画效率和性能的支持还存在一些问题,性能上不如原生。这时候只能嵌入原生的组件,但工作量可能会比较大。
  • 学习成本较高: 在某些地方开发者依然需要为 iOS 和 Android 平台提供两套不同的代码,比如在原有项目的基础上嵌入RN时,需要根据平台进行封装和配置。在官方文档里就可以发现不少组件和API都区分了 Android 和 iOS 版本。即使是共用组件,也会有平台独享的函数。还有就是有些地方对 iOS 和 Android 的原生细节依然有所依赖,所以需要开发者若不了解原生平台,可能会遇到一些坑。
  • 配置比较麻烦:首先要安装 Node、Watchman、Yarn、Xcode(iOS)、CocoaPods(iOS)、Android Studio(Android)、JDK(Android) 等依赖。如果只开发单个平台,那就安装相应平台的依赖就行了。因为很多库都是在国外,所以还要切换镜像。

语法介绍

JSX

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 组件和原生组件的对应关系:

rn 可以使用axios吗 rn开发使用的什么语言_ios

转换成代码大概是这样的:

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,哪怕它们用起来更麻烦。

ScrollViewFlatList应该如何选择?ScrollView 会简单粗暴地把所有子元素一次性全部渲染出来。其原理浅显易懂,使用上自然也最简单。然而这样简单的渲染逻辑自然带来了性能上的不足。想象一下你有一个特别长的列表需要显示,可能有好几屏的高度。创建和渲染那些屏幕以外的 JS 组件和原生视图,显然对于渲染性能和内存占用都是一种极大的拖累和浪费。

FlatList 使用 tips
  • keyExtractor 属性赋值,给 item 指定唯一的 Key,也可以给 item 的 key 属性赋值,像这样 <Item key={item.id} />。这么做是为了区分同类元素的不同个体,以便在刷新的时候能确定变化的 item,就只更新该 item,其他复用之前的,减少重新渲染的开销。如果不给 FlatListkeyExtractor 属性赋值,RN 会默认使用 item.key 作为 Key 值,如果 item.key 也不存在,则会使用数组下标(使用数组下标可能会导致组件出现数据错乱的问题)。使用方法如下:

// 属性声明
(item: object, index: number) => string;

// 使用
keyExtractor={(item, index) => item.id}
  • 如果列表的行高是固定的,建议给 getItemLayout 属性赋值,可以避免动态测量内容尺寸的开销,对于元素较多的列表,可以极大的提高性能。需要注意的是,如果指定了分割线 ItemSeparatorComponent,则需要把分割线的尺寸也考虑到 offset的计算中,getItemLayout 的使用方法如下:
// 属性声明
(data, index) => { length: number, offset: number, index: number};

 // 使用
getItemLayout={(data, index) => (
   {length: item_height, offset: height * index, index}
 )}

布局

RN 中的视图布局使用的是一种实现了Flexbox规范且基于CSS的跨平台布局系统。该系统可以在不同的屏幕尺寸上提供一致的布局结构。

随着这个系统的不断完善,Facebook决定对它进行重启发布,并取名YogaYoga是一个表达性极强的布局库,并没有实现CSS的所有内容。没有支持表、浮动等某些类似的CSS概念。Yoga也不支持对于布局无影响的部分、比如颜色和一些背景属性。Yoga是基于C实现的。之所以选择C,首先当然是从性能方面考虑的。基于C实现的Yoga比之前Java实现在性能上提升了33%。其次,使用C实现可以更容易地跟其它平台集成。如果对Yoga感性去,可以到官方文档去仔细研究

一般来说,使用flexDirectionalignItemsjustifyContent三个样式属性就已经能满足大多数布局需求。

什么是 FlexBox

FlexBox的全称是Flexible Box,意为弹性布局,用于为盒状模型提供最大的的灵活性。是 Web 端的一种布局方案,RN 中的 Flexbox 的工作原理和 Web 上的 CSS 基本一致,但是也存在少许差异。首先是默认值不同:flexDirection的默认值是column而不是row,而flex也只能指定一个数字值,并且有一些属性在 RN 里面也是没有的,比如 Flex-Flow。下面是一个 flex 容器的图解:

rn 可以使用axios吗 rn开发使用的什么语言_rn 可以使用axios吗_02

主要属性
  • flex
    flex属性决定元素在主轴上如何填满可用区域。整个区域会根据每个元素设置的 flex 属性值被分割成多个部分。在下面的例子中,在设置了flex: 1的容器 view 中,有红色,黄色和绿色三个子 view。红色 view 设置了flex: 1,黄色 view 设置了flex: 2,绿色 view 设置了flex: 31+2+3 = 6,这意味着红色 view 占据整个区域的1/6,黄色 view 占据整个区域的2/6,绿色 view 占据整个区域的3/6

rn 可以使用axios吗 rn开发使用的什么语言_react native_03

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的概括:

rn 可以使用axios吗 rn开发使用的什么语言_react native_04

  • justifyContent
    在组件的 style 中指定justifyContent可以决定其子元素沿着主轴排列方式

rn 可以使用axios吗 rn开发使用的什么语言_rn 可以使用axios吗_05

  • alignItems
    在组件的 style 中指定alignItems可以决定其子元素沿着次轴(与主轴垂直的轴,比如若主轴方向为row,则次轴方向为column)的排列方式

数据传递与更新

Props

在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

state是一个普通 JavaScript 对象,可以由用户自定义(只能在构造函数里面自定义)。stateprops类似,但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需要注意的点
  1. 不要直接修改state,例如:
this.state.date = new Date()

// 上面的代码并不会重新渲染组件,应该要这样更新
this.setState({ date: new Date() })
  1. state的更新可能是异步的,所以如果下一次数据更新依赖上一个state的更新值的话,可能会导致结果不准确。例如:
// 计数器的更新
this.setState({
  counter: this.state.counter + this.props.increment,
});

// 要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
  1. state的更新会被合并,因为RN会延迟调用setState(),然后将多个setState()合成一个调用。

Touchable 系列组件

  • 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;

以上的组件仅TouchableOpacityTouchableHighlight支持 Style 样式属性,另外两个都不支持。并且Touchable 系列组件只支持一个子节点(不能没有子节点也不能多于一个)。如果你希望包含多个子组件,可以用一个 View 来包装它们。

生命周期

每个组件都包含 生命周期方法,这些方法都可以被重写,在运行过程中特定的阶段会调用这些方法。你可以使用此以下图表作为速查表。在下述说明中,常用的生命周期方法会被加粗,没加粗的则表示使用较少的生命周期函数。

rn 可以使用axios吗 rn开发使用的什么语言_rn 可以使用axios吗_06

RN 的生命周期可以大致分为4个阶段:

rn 可以使用axios吗 rn开发使用的什么语言_rn 可以使用axios吗_07

  • Mounting :挂载阶段,主要是创建组件实例并将其插入到 DOM 树中。方法调用顺序如下:
  • constructor():
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()
  • Updating:更新阶段,主要是通过接受新的参数来更新组件。方法调用顺序如下:
  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()
  • Unmounting:卸载阶段,从实际的 DOM 树中删除一个组件。方法调用顺序如下:
  • componentWillUnmount()
  • Error Handing:错误处理的阶段,在 Render 期间、生命周期的方法、子组件的构造方法中出现错误时,会进入该阶段。方法调用顺序如下:
  • static getDerivedStateFromError()
  • componentDidCatch()

生命周期方法的详细说明可以在官方文档查看。

异步编程

RN 中的线程

RN中有4个线程:

  • UI线程: 用于iOSAndroid原生组件的呈现和交互,也是应用的主线程。
  • 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

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是一个对象,所以可以用构造函数来生成Promise实例,代码如下:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 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);
		});
	});
});

代码量更多不说,可读性也很差。

async / await

与单纯的回调函数相比,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/awaitPromise的语法糖,所以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 需要注意的点
  • 虽然使用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内置了三种网络请求的方式fetchXMLHttpRequestWebSocketXMLHttpRequest(XHR)WebSocket都是由两部分组成: “前端” 和 “后端”。前端负责与JavaScript交互,后端负责在原生平台上转换JavaScript发送过来的请求为原生系统自己的请求。而fetch是基于XMLHttpRequest的,所以RN网络请求逻辑实际上分为两个部分:一个是JS的运行环境,一个是原生平台的运行环境。你在JS层调用网络请求时,其实是经历了两个过程才到达真正的服务器端。

使用 Fetch

React Native 提供了和 web 标准一致的Fetch API,用于满足开发者访问网络的需求。如果你之前使用过XMLHttpRequest(即俗称的 ajax)或是其他的网络 API,那么 Fetch 用起来将会相当容易上手。

1.发起请求

要从任意地址获取内容的话,只需简单地将网址作为参数传递给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或者抓包工具CharlesFiddler等来进行观测。

2.处理服务器的响应数据

网络请求天然是一种异步操作,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);
  }
}

使用XMLHttpRequest

因为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

WebSocket 支持

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 可以使用axios吗 rn开发使用的什么语言_封装_08

调试工具的简单介绍

代码调试

一般都是通过 RN 内置的开发菜单的 Debug选项来打开调试工具,如果是离线包调试,则可以通过弹窗的形式打印日志、或者其他的工具进行调试。选择 Debug后,默认是打开 Chrome开发者工具,因为 RN 是用 js 来开发的,所以调试 RN 的流程基本和网页、小程序的流程一样。以下是 Chrome开发者工具的页面:

rn 可以使用axios吗 rn开发使用的什么语言_Text_09

橙色方框部分是调试工具的菜单栏,可以根据需要选择相应的菜单,比较常用的选项是 ConsoleSource,其他的几乎不怎么用到 。Console 主要用于日志、警告、错误的打印,还可以在这里打印当前上下文的变量值,以及执行 js 语句。 Source 主要用于断点调试,即当前截图显示的页面,左侧的红色方框则是项目的文件目录。

UI 调试

目前 Chrome的开发者工具只能调试代码逻辑,不能看到 App 的用户界面和视图架构,不过可以使用 RN 自带的 UI 调试工具,就在开发菜单的 Show Inspector选项里面,如下图:

rn 可以使用axios吗 rn开发使用的什么语言_ios_10

选中底部的 Inspect,点击你想查看的视图组件,就会出现该组件的布局信息。红色方框里表示的是视图的层级,橙色方框里的是当前视图的 css 代码,蓝色方框里的是当前组件的代码的位置,紫色方框里的是当前视图的约束。

网络调试

根据官方文档的说明,Chrome的开发者工具目前不能直接观测到 RN 中的网络请求,所以如果需要进行网络调试,可以使用更强大的调试工具 react-native-debugger,或者也可以使用抓包工具,比如 fiddlercharles 来进行调试。

性能监控

点击开发菜单的 Show Perf Monitor 就可以打开性能监控工具,可以通过上面的信息大致分析 RN 的性能问题。

rn 可以使用axios吗 rn开发使用的什么语言_ios_11

  • RAM: 当前应用的内存。
  • JSC: JavaScriptCore 托管堆的大小,只在触发垃圾收集的时候更新。
  • Views: 总共有两个数字,上面的数字表示当前屏幕上显示的视图数量,下面的数字则表示已经进行过布局和绘图计算的视图总数(包括屏幕外以及能够被合并的视图,例如嵌套的文本)。这两个数字的差值越小,额外的布局和绘图计算就会越少,则当前页面性能就会越好。
  • UI: UI 线程的帧率
  • JS: RN JS 线程的帧率。对大多数 RN 应用来说,业务逻辑是运行在 JS 线程上的。这是 React 应用所在的线程,也是发生 API 调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果 JavaScript 线程有一帧没有及时响应,就被认为发生了一次丢帧。
  • 下面的列表是各个模块的执行时间

参考文章