异步解决方案之Promise和async/await
Promise对象
异步与同步相比,最难以掌控的就是异步的任务会什么时候完成和完成之后的回调问题。
难以掌控的触发状态,让你自己写的代码当时还可以读懂,但是过几天半个月之后如果不重新盘一边逻辑,你哪知道哪个内容会先执行借用这么一个例子
document.addEventListener('click', function handler(evt) {
setTimeout(function () {
ajax('some.url', function(txt) {
if(txt == 'hello') {
handler()
} else {
request()
}
})
}, 1000)
})
首先 执行listern()
其次 doSomething()
500ms(或者更远)后执行ajax()
ajax完成后
如果text === hello 执行handler()
如果text === world 执行request()
难受吗???
事实证明 你需要一个承诺
当你把一件事情交给别人去做(可能马上就能完成的也可能是需要一段时间的)这个人在任务完成或者失败后都会给你一个回应,这样的人你是不是特别放心的把事情交给他,他没回应你那么他是正在办事、回应你了就是成功了或者失败了。
在javascript中这样的人就是Promise。
Promise的实例有三个状态,Pending(进行中)、Resolved(已完成)、Rejected(已拒绝)。当你把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
言归正传:写一个简单的promise
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
当Promise执行的内容符合你预期的成功条件的话,就调用resolve函数,失败就调用reject函数,这两个函数的参数会被promise捕捉到。可以在之后的回调中使用。
创建一个承诺我们已经做完了,那么如何使用承诺后的结果呢?
promise.then(res=>
{console.log(res);//在构造函数中如果你执行力resolve函数就会到这一步
},err=>{
// 执行了reject函数会到这一步
console.log(err);
})
例1
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
例2
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
then方法接收两个函数,第一个是承诺成功(状态为resolved)的回调函数,一个承诺失败(状态为rejected)的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
let p = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'success');
});
p.then(
res => {
console.log(res);
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'success');
});
}
)
.then(
res => console.log(res)
);
说完串行了,那么并行怎么办???当有多个异步事件,之间并无联系而且没有先后顺序,只需要全部完成就可以开始工作了。
串行会把每一个异步事件的等待时间进行一个相加,明显会对完成进行一个阻塞。那么并行的话该怎么确定全部完成呢?
Promise.all
Promise.all 接收一个数组,数组的每一项都是一个promise对象。
const p = Promise.all([p1, p2, p3]);
当数组中所有的promise的状态都达到resolved的时候,Promise.all的状态就会变成resolved,如果有一个状态变成了rejected,那么Promise.all的状态就会变成rejected(任意一个失败就算是失败),这就可以解决我们并行的问题。调用then方法时的结果成功的时候是回调函数的参数也是一个数组,按顺序保存着每一个promise对象resolve执行时的值。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
Promise.race
Promise.race 竞速模式 也是接受一个每一项都是promise的数组。
const p = Promise.all([p1, p2, p3]);
但是与all不同的是,第一个promise对象状态变成resolved时自身的状态变成了resolved,第一个promise变成rejected自身状态就会变成rejected。第一个变成resolved的promsie的值就会被使用。
const pro1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 1)
})
const pro2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 2)
})
const pro3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, 3)
})
Promise.race([pro1, pro2, pro3]).then(res => {
console.log(res)
}, err => {
console.log('reject')
console.log(err)
})
// 1
async 函数
(1)基本使用
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
下面是一个例子。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。
下面是另一个例子,指定多少毫秒后输出一个值。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
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 () => {};
(2)返回 Promise 对象
async函数返回一个 Promise 对象。
async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
(3)await 命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
上面代码中,await命令的参数是数值123,这时等同于return 123。
另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。
await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world