javascript异步编程解决方案
用JS写程序,避免不了要进行异步编程,这篇文章将介绍JS中的几种异步编程的解决方案,若有错误请指正。
1、Promise
大名鼎鼎的promise,作为异步编程的首选被众多前端工程师采用,但是关于promise的细节这里做一些补充。
他是最早由社区提出的一种解决异步方案,后来被ES6收入规范,原生提供了Promise对象。promise和以往的回调函数相比有更优雅和合理的处理方式以及写法。
事实上promise是一个对象。单词也是承诺的意思,在JS中表示对当前异步状态的留存。 这个对象的特点有以下两点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
(2)Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
(3) Promise 新建后就会立即执行。
用法:
下面代码创造了一个Promise实例。
const promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
console.log('hello')
} else {
reject(error);
}
});
这个例子中有两个函数,resolve和reject,这两个函数是由promise内置的,不用自己声明。理解这两个函数对于改变promise的状态至关重要。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;在调用resolve后后面的代码仍然会执行,上列中的‘hello’会输出在控制台中,除非我们用return返回resolve。
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
then方法:
then方法是定义在promise原型对象上的。then方法可以接受promise返回的结果,接受两个回调函数作为参数,分别代表成功和失败。 then方法返回的是一个新的Promise实例, 因此可以采用链式写法。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
链式写法:
promise.then(function(value) {
// success
}, function(error) {
// failure
}).then(res =>{
})
catch方法:
catch用于指定发生错误时的回调函数。
const promise = new Promise(function(resolve, reject) {
throw new Error('error');
});
promise.catch(function(error) {
console.log(error);
});
当promise抛出错误时会被catch捕获,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
catch方法也可以链式调用:
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
如果promise抛出错误,而没有catch方法,此错误不会被抛出到promise外,也不会影响外部代码的执行。
finally方法
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {console.log('world')});
'world’始终会输出在控制台。
all方法
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p1,p2,p3三个promise对象的状态决定p的状态。影响策略如下
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
race方法
在形式上race与all相同:
const p = Promise.race([p1, p2, p3]);
不同点在于:
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise,实例的返回值,就传递给p的回调函数。
即所谓的竞争状态。
allSettled方法
从字面上解读,allSettled可以理解为所有的promise都执行完或者状态确定了就执行allSettled方法。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
goOn();
如上所示,
数组promises包含了三个请求,只有等到这三个请求都结束了(不管请求成功还是失败),goOn()才会执行。
any方法
Promise.any([
fetch(),
fetch(),
fetch()
]).then((first) => { // 只要有一个 fetch() 请求成功
console.log('first');
}).catch((error) => { // 所有三个 fetch() 全部请求失败
console.log('error');
});
从上述的实例来看,any方法接受多个promise对象,一旦有一个成功则any的状态就变为fulfilled,如果三个promise都为失败状态,则any状态为rejected状态。
当然关于promise还有其他一些方法,比如try()方法,这里不多介绍,大家可以看阮一峰老师关于promise的介绍(https://es6.ruanyifeng.com/#docs/promise)
下面第二种异步编程方法
2、async
async在2017年被写入es标准中,作为异步解决方案,他更优雅。
async函数返回一个 Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
如:
async function A() {
const D = await B();
const E = await C();
return E;
}
当函数被async声明时,表明其内部含有异步操作,await 后面的B()方法作为一个异步求值方法,当B请求结束时程序会进行下以步,开始执行函数C()。这几乎使用同步代码的形式执行了两个异步操作。细心地朋友会发现这类似于生成器函数的指针操作。
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
await函数
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
如果await 返回了一个reject状态,reject的参数会被catch方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
重点
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
如果要跳过错误继续执行,则需要用try…catch捕获此次错误。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
3、Generator
Generator字面竟然是发电机?,为什么要用这个词,着实也让我一脸懵。但也是 ES6提供的一种异步编程解决方案,从名字就可以看出来这个玩意儿和传统的函数写法不同。
function* F() {
yield 'hello';
yield 'world';
return 'ending';
}
let w = F();
形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态
但是!调用F()后生成器函数并不执行。我们需要调用函数内部的next方法才能执行
w.next()
// { value: 'hello', done: false }
w.next()
// { value: 'world', done: false }
w.next()
// { value: 'ending', done: true }
调用 Generator 函数,返回一个遍历器对象,代表 Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
如果next方法已经执行到末尾,那么再次调用后结果为:
w.next()
// { value: undefined, done: true }
Generator内部也可以不用yield表达式,那么此时:
function* F() {
return 'ending';
}
let w = F();
仍然要用 w.next()才能获取返回值
重点:
next方法可以传值,如下:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next(2) // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
传值只对当前的yield表达式有效。
那么如何实现异步操作?
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
var g = function* (){
var f1 = yield fetch('fileA');
var f2 = yield fetch('fileB');
// ...
var fn = yield fetch('fileN');
};
run(g);
上述代码用类似递归的方式实现了多个异步请求的执行
三种异步方法的比较:
首先:Promise方法虽然解决了异步操作,但是有很多自身的API,语义化不够明显,本身的then方法链式调用在面对多个请求时,多个链式操作不够友好
其次: Generator 函数,优点是语义比 Promise 写法更清晰,但是奇怪的用法可能需要适应,不易理解。
最后:Async 函数,Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码.
那么如果你在解决异步操作时,会选择哪种呢?