JS基础:异步编程之Promise、async-await
目录
一. Promise
1. 为什么要使用Promise
2. Promise是什么
3. 如何使用Promise
4.
fetch
、AsyncStorage
使用示例二.
async-await
一. Promise
1. 为什么要使用Promise
关于事件循环、线程、队列、同步任务、异步任务,这里就不展开了,简单说下它们在JS里的情况。
为了程序执行的简单,JS被设计为单线程的,也就是说JS里的所有任务都是在主线程里执行的,下一个任务必须得等上一个任务执行完毕才能执行,这也就是同步任务。但是如果一个任务的执行时间可能很长(如一个任务里包含了网络请求、数据库读写等IO操作),它就会阻塞主线程,导致后面的任务也无法执行,不过让后面的任务等也不是不行,如果是因为某个任务计算量大而导致CPU忙不过来,那这个等就是不可避免的,也是有效的,但很多情况的等却是任务中IO操作的部分出结果很慢,导致我们一直拿不到结果,CPU就只能在那闲着干等,等到结果后再继续执行该任务。于是JS的设计者就设计,CPU完全可以不管某个任务中IO操作的部分,当某个任务执行IO操作的时候,就挂起这个任务,并把这个任务放到一个专门的队列中去,让CPU继续执行后面的任务,等IO操作返回了结果,再把这个任务从队列里拿出来放到主线程中继续执行下去,于是这种任务就成了异步任务,它不会阻塞主线程,而这种一遍一遍不停地检查异步任务是否该继续执行的机制就是JS里面的事件循环机制(Event Loop)。
一个异步任务的通常写法都是:IO操作 + 回调函数。IO操作为一种耗时操作,回调函数用来指定耗时操作结束后接下来该任务要干什么。JS里设计如果一个异步任务没有回调函数,那么它在执行IO操作被挂起后,就不会把它放入任务队列中,那么当IO操作返回结果后,它也就不会再次进入主线程继续执行了,因为它没有用回调函数指定下一步要干什么。但如果异步任务指定了回调函数,那么当异步任务重新进入主线程时,就是执行对应的回调函数。
下面我们举例子来看看异步任务的编写。
假定f1
要做个异步任务,f2
是f1
的回调函数。
function f1(callback) {
// f1任务的耗时代码
// f1任务的耗时代码执行完后,执行回调函数
callback();
}
f1(f2);
使用回调函数法来实现异步任务的优点是简单和容易理解,但是却可能出现下面这样的使用情况。
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
async(1, function (value) {
async(2, function (value) {
async(3, function (value) {
async(4, function (value) {
async(5, function (value) {
async(6, final);
});
});
});
});
});
// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成: 12
如果像上面这样,异步任务的回调函数又是一个异步任务,那回调函数就会一直嵌套下去,此时代码的结构就有点乱了,我们也无法从代码中清晰地阅读出每个异步任务的耗时任务完成后,接下来要做什么。
Promise的出现,就是为了解决异步任务的回调函数可能过于臃肿和不易阅读的问题,下面我们来详细看看它。
2. Promise是什么
Promise的主要用途就是通过then
、catch
等方法来给异步任务设置回调,代替掉原来回调函数的那种实现方案,从而使得整个异步任务的流程更清晰,代码更易读。例如上面的例子,使用Promise后如下。
(new Promise(stpe1))
.then(step2)
.then(step3)
.then(step4);
Promise是一个对象,也是一个构造函数。Promise构造函数接受一个函数f1
作为参数,f1
里面是异步任务的代码,返回的p1
就是一个Promise对象。下面一小节我们会做详细的介绍。
var p1 = new Promise(f1);
Promise对象有三种状态。
- 异步操作进行中(
pending
) - 异步操作成功(
fulfilled
) - 异步操作失败(
rejected
)
这三种状态之间的转变只有两种可能,而且一旦状态发生变化,就凝固了,不会再发生变化。
- 异步操作进行中 ——> 异步操作成功
- 异步操作进行中 ——> 异步操作失败
因此,Promise对象的最终状态只有两种。
- 异步操作成功
- 异步操作失败
注意:
fulfilled
和rejected
两种状态合在一起又可以称为resolved
状态(已定型),但是为了行文方便,本篇后面的resolved
状态统一只指fulfilled
状态,不包含rejected
状态。
3. 如何使用Promise
- 第一步:使用Promise构造函数,通过固定的格式来包裹异步任务,并将异步任务的执行结果或错误传递出去
有了Promise之后,如果我们想给某个异步任务添加回调函数,就不是编写一个普通的函数,在内部做异步任务并执行回调函数了,而是直接使用Promise构造函数,通过固定的格式来包裹异步任务,并将异步任务的执行结果或错误传递出去。
const promise = new Promise(function (resolve, reject) {
// some code...
if (/* 异步任务执行成功 */){
resolve(value);
} else { /* 异步任务执行失败 */
reject(error);
}
});
上面代码中,Promise构造函数接收一个函数作为参数。该参数函数的两个参数分别是而且必须是resolve
和reject
,它们俩是JS提供的系统函数,不需要我们自己部署,我们只要这么固定地写就可以了;该参数函数的执行体就是要执行的异步任务,并通过resolve(value)
和reject(error)
固定的写法,将异步任务的执行结果或错误传递出去,执行体会在Promise对象创建之后立即执行。
通过以上的固定写法,我们知道resolve
函数会在异步操作成功时触发,并将异步操作的结果作为参数传递出去,这个函数的执行会把Promise对象的状态从pending
变为resolved
;reject
函数会在在异步操作失败时触发,并将异步操作的错误作为参数传递出去,这个函数的执行会把Promise对象的状态从pending
变为rejected
。
- 第二步:使用Promise的
then
方法和catch
方法,为异步任务添加回调函数
在第一步中,我们并没有直接为异步任务添加回调函数,而仅仅是通过resolve(value)
和reject(error)
把异步任务的结果或错误传递出来了,现在我们来为异步任务添加回调函数。
Promise对象生成之后,我们可以通过它的then
方法,分别指定它变为resolved
状态(即异步任务执行成功)和rejected
状态(即异步任务执行失败)后的回调函数。
promise.then(function (value) {
// success
}, function (error) {
// failure
});
then
方法可以接受两个回调函数作为参数。第一个回调函数会在异步任务执行成功调用,第一步传出来的value
就能在这里接收到;第二个回调函数会在异步任务执行失败调用,第一步时传出来的error
就能在这里接收到。其中第二个函数是可选的,不一定要提供。
同时then
方法执行后的返回值又是一个新的Promise对象(注意不是原来那个Promise对象了),因此可以对then
方法采用链式写法,这时上一个then
方法参数函数的执行结果,会自动传递给下一个then
方法的参数函数作为参数。
promise.then(function (异步任务的执行结果) {
// ...
return 结果1;
}).then(function (结果1) {
// ...
return 结果2;
}).then(function (结果2) {
// ...
});
除了then
方法之外,Promise还有一个catch
方法,它其实是.then(null, rejection)
或.then(undefined, rejection)
的别名,可以专门用来指定Promise对象变为rejected
状态(即异步任务执行失败)的回调函数。
promise.then(function () {
// ...
return 结果1;
}).then(function (结果1) {
// ...
return 结果2;
}).then(function (结果2) {
// ...
}).catch(function (error) {
// ...
});
catch
方法可以捕捉它上面所有then
方法的错误,使用catch
方法捕捉错误要比使用then
方法既捕捉成功也捕捉的代码看起来清晰明了。因此我们推荐,使用then
方法提供异步任务成功的回调,而使用catch
方法提供异步任务失败的回调。
4.fetch
、AsyncStorage
使用示例
// ProjectRequest.js
/**
* RN提供的fetch方法,是异步的,它本身就会返回一个Promise对象。因为这里我们对它进行了封装使用,所以外面又包了一层Promise,来给fetch这个异步任务提供回调,这样外界才能拿到它的结果。
*
* @param url
* @param params
* @returns {Promise<any> | Promise}
*/
static post(url, params) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
})
.then(response => {
if (response.ok) {
// 请求到的response其实是一个Response对象,它是一个很原始的数据格式,我们不能直接使用,先获取它的JSON字符串文本格式
return response.text();
} else {
throw new Error('网络请求失败!');
}
})
.then(responseText => {
// 然后把JSON字符串序列化为JS对象
const responseJSObj = JSON.parse(responseText);
// 把请求成功的数据传递出去
resolve(responseJSObj);
})
.catch((error) => {
// 把请求失败的信息传递出去
reject(error);
})
})
}
// ThemeDao.js
/**
* 读取主题色
* RN提供的AsyncStorage,它的读取和写入操作都是是异步的,只通过回调函数的方式来告诉我们结果。因为这里我们对它进行了封装使用,所以外面又包了一层Promise,来给AsyncStorage这个异步任务提供回调,这样外界才能拿到它的结果。
*
* @returns {Promise<any> | Promise}
*/
static getThemeColor() {
return new Promise((resolve, reject) => {
AsyncStorage.getItem(THEME_COLOR, (error, value) => {
if (error) {
reject(error);
} else {
if (!value) {// 数据库中还没有存主题色
// 那就搞个默认的主题色
value = AllThemeColor.Default;
// 存起来
this.saveThemeColor(value);
}
// 传出去
resolve(value);
}
});
});
}
二. async-await
async-await
的主要作用就是用来将一个异步任务变成同步的。
// 存储的数据为:{'hey': '你好'}
_read() {
console.log(1);
AsyncStorage.getItem('hey', (error, value) => {
if (error) {
console.log('读取数据出错:', error);
} else {
console.log(2);
console.log(value);
console.log(3);
}
});
console.log(4);
}
比如上面这串代码,是从数据库里读取一些数据,因为AsyncStorage.getItem
这个操作是异步的,所以会依次输出1、4、2、你好、3。
但有时候,我们希望确确实实从数据库读到了数据再执行后面的操作,而不是把操作放到异步操作的回调里执行,此时就可以用async-await
将一个异步任务变成同步的。
async _read() {
console.log(1);
await AsyncStorage.getItem('hey', (error, value) => {
if (error) {
console.log('读取数据出错:', error);
} else {
console.log(2);
console.log(value);
console.log(3);
}
});
console.log(4);
}
这样,代码在执行到await
的地方就会阻塞住,直到它后面的异步操作执行完毕,才会执行后面的语句,async
只是个标识符,没什么实际的意义。这样会依次输出1、2、你好、3、4。