ES6 - 异步处理技术(Promise、async、await)优化
异步场景
现在有个程序输入电影名字就去下载这部电影,然后下载的时候只能同时下载一部,下载完成一部再能开始下一部的下载。下载成功会有相关提示,下载失败也有相关提示。如下。
// 这个函数有三个参数 1.要下载的电影名字 2.下载成功的回调 3.下载失败的会回调
function download(name, resolve, reject) {
// 模拟正在下载
console.log("正在下载......");
// 模拟一秒钟后下载成功或是失败
setTimeout(() => {
if(Math.random() < 0.7) {
resolve(`下载《${name}》成功`);
}else {
reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
}
}, 1000)
}
// 调用这方法下载《电影1》
download("电影1", data => {
console.log(data);
}, reason => {
console.log(reason);
})
上面是下载一部电影的场景,那么如果要下载4部呢,下载四十部呢,要怎么调用这个函数。在说明一下要求:下载多个电影的时候,只有前一个下载任务完成,下一个下载任务才能开始。下面是示例代码。
// 下载《电影1》
download("电影1", (data) => {
console.log(data);
// 下载成功继续下载《电影2》
download("电影2", (data) => {
console.log(data);
// 下载成功继续下载《电影3》
download("电影3", (data) => {
console.log(data);
// 下载成功继续下载《电影4》
download("电影4", (data) => {
console.log(data);
// 到这里全部下载任务完成
console.log("全部下载任务完成")
}, (reason) => {
// 如果失败,后续下载任务直接崩溃
console.log(reason);
})
}, (reason) => {
// 如果失败,后续下载任务直接崩溃
console.log(reason);
})
}, (reason) => {
// 如果失败,后续下载任务直接崩溃
console.log(reason);
})
}, (reason) => {
// 如果失败,后续下载任务直接崩溃
console.log(reason);
})
可以看到,才下载4部电影就要写这么长的代码,如果是40部呢!!!
- 上面就是一个异步的场景,通过回调函数的方式来解决。
- 上面可以看到回调一层一层的嵌套,形成了传说中的回调地狱。
- 以前没有一个好的方法解决异步问题只能使用回调的方式,但是现在ES6推出了一个叫Promise的东西,就是专门来解决回调的。这个时候我们的Promise闪亮登场。
Promise规范
概述
- 什么是范?
- 就是说,我规定这个地方应该有什么东西,这个东西应该能干什么事。跟规划一个道理
- 什么是Promise?
- Promise是一套专门处理异步场景的规范,他能有效的避免回调地狱,使代码清晰统一。
Promise的规范有什么
- 所有的异步场景,都可以看作是一个异步任务,每个任务在js中应该表现为一个对象,这个对象成为Promise对象。如下
- 使用网络请求数据
- 延时操作
- 每个Promise对象都应该有两个阶段三个状态;
- 未决阶段(unsettled)
- 挂起状态(pending)
- 已决阶段(settled)
- 成功状态(fulfilled)
- 失败状态(rejected)
-
状态转变过程,转变过程都是从
未决状态(unsettled)
到已决状态(settled)
。-
挂起(pending) --> 完成(fulfilled)
的过程,称之为resolve
。 -
挂起(pending)--> 失败(rejected)
的过程,称之为reject
。 -
成功状态可以有成功的相关数据,失败有失败的相关错误信息。后续代码演示
-
-
改变状态之后可以针对完成或者失败进行后续操作。
- 针对“完成状态”的后续处理称之为
onFulfilled
- 针对失败的后续处理成为
onRejected
Promise API
Promise构造函数
ES6提供了一个Promise构造含数,用来帮我们创建Promise对象,我们每个异步任务都是一个对象。这个函数有两个参数,一般我们第一个参数写成resolve
,第二个参数写成reject
。
// 创建一个Promise对象
const demo = new Promise((resolve, reject) => {
......
})
// 可以查看一下当前这个对象的状态
console.log(demo); // Promise { <pending> }
这个时候我们就可以用一个异步场景来使用一下这个Promise。比如现在小明去贩卖机买瓶可乐,但是这个贩卖机有时候会出问题,给钱1秒钟后会有50%的几率会把可乐吐出来50%的几率机器会出问题没有反应。
const buy = new Promise((resolve, reject) => {
console.log("付钱了,等待贩卖机出可乐...");
setTimeout(() => {
// 模拟50%的几率出可乐,和不出可乐
if(Math.random() - 0.5 > 0) {
// 购买成功,resolve是一个函数,这个函数会把promise对象的状态改成 fulfilled
resolve("购买成功, 贩卖机没有出问题!!!");
}else {
// 购买失败,reject是一个函数,这个函数会把promise对象的状态改成 rejected
reject("完蛋了, 3块钱没了, 人品也太差了点!!!");
}
}, 1000)
})
构造函数里面的代码会马上运行。但是还有一个问题,就是现在异步任务也创建了,也已经从未决阶段变成已决阶段,那完成然后呢,然后要干什么事情呢,这里还没有定义,还没办法定义已决状态之后要干什么事。这个时候Promise对象的then
方法闪亮登场。
then方法
这个方法传入两个回调函数,第一个函数是状态为fulfilled
时的回调,第二个函数是状态rejected
时的回调。如下
// 调用上面创建的对象的then方法,输出对应状态的状态信息
buy.then((data) => {
console.log(data);
}, (reason) => {
console.log(reason);
})
/*
可能会输出以下信息:
付钱了,等待贩卖机出可乐...
完蛋了, 3块钱没了, 人品也太差了点!!!
*/
优化下载电影的方法
// 改成返回一个Promise对象
function download(name) {
return new Promise((resolve, reject) => {
// 模拟正在下载
console.log(`正在下载《${name}》......`);
// 模拟一秒钟后下载成功或是失败
setTimeout(() => {
if (Math.random() < 0.8) {
resolve(`下载《${name}》成功`);
} else {
reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
}
}, 2000)
})
}
download("电影1").then(data => {
console.log(data);
}, reason => {
console.log(reason);
})
这个时候,还没有解决回调地狱的问题。
catch
这个方法是专门处理失败的回调,比如说,一个任务成功了但是成功了你不想做任何操作,当失败的时候再进行相应的操作就可以使用这个方法。例子:你去贩卖机买一瓶可乐,如果给了钱贩卖机也把可乐吐出来了,这时候任务就是成功了不做任何后续操作。但是如果你付了钱贩卖机没有把可乐吐出来,那你就会想我的血汗钱怎么能说没就没呢,这个时候你就要吧这个贩卖机拆了,代码如下。
// 一个买可乐的任务
const buy = new Promise((resolve, reject) => {
console.log("付钱,等待出可乐......");
// 一秒钟之后没有出可乐
setTimeout(() => {
reject();
}, 1000)
})
// 我们使用then方法第一个参数是成功的回调,第二个参数是失败的回调,如果只想处理失败的话可以写成下面这样
buy.then(null, () => {
console.log("你这小贩卖机敢吞我钱,看我不把你拆了");
})
我们还可以使用更简洁的方法处理失败状态,那就是使用catch
方法。就不用像上面那样在第一个参数写一个null
// 只处理失败状态
buy.catch(() => {
console.log("你这小贩卖机敢吞我钱,看我不把你拆了")
})
buy.then(null, () => {})
= buy.catch(() => {})
。
链式调用
什么是链式调用:
在看我们去贩卖机买可乐的例子,50%几率出可乐50%几率不出可乐。如果出可乐了是不是有对应的操作,不出可乐又有对应的操作。
- 出可乐了
- 买可乐 -> 拿到可乐 -> 打开拉环
- 没有出可乐
- 买可乐(没有买到) -> 既然没有买到是不是拿到可乐和打开拉环这个任务就没办法执行了
我们买可乐是一个任务,然后买了拿到可乐也是一个任务,拿到可乐打开拉环又是一个任务。所以我们的
then
方法也是一样,这个方法会返回一个新的Promise对象
。代码解释如下
- 买可乐(没有买到) -> 既然没有买到是不是拿到可乐和打开拉环这个任务就没办法执行了
我们买可乐是一个任务,然后买了拿到可乐也是一个任务,拿到可乐打开拉环又是一个任务。所以我们的
const buy = new Promise(resolve => {
// 直接买到可乐了
resolve("买到了");
})
// 针对买到进行后续操作
const result = buy.then(data => {
console.log(data); // 买到了
})
// 我们看看result是一个什么东西
console.log(result); // Promise { <pending> }
我们可以看到上面then方法的返回的也是一个Promise对象
。
既然如此,返回的对象我们可以在次调用then方法,如下
const buy = new Promise(resolve => {
resolve("买到了");
})
buy.then(() => {
console.log("拿可乐");
}).then(() => {
console.log("打开拉环");
})
我们根据前一个任务的返回值再继续调用方法,就叫做链式调用。我们的then
方法会返回Promise对象
(新任务)那么这个对象也是会有状态的,那这个状态是怎么改变的呢,then方法里面又没有resolve和reject可以调用让其改变状态。
新任务的状态取决于后续处理
- 如果没有后续处理,那么新任务的状态跟旧任务的状态一致,相关数据就是旧任务的数据。如下
const buy = new Promise((resolve) => {
resolve("买到了");
})
const result = buy.catch(() => {
console.log("没买到")
});
// 这里得延迟一点时间再输出状态不然状态永远为pending
setTimeout(() => {
console.log(result); // Promise { '买到了' }
})
上面刚开始直接把状态改为成功,并返回相关数据,但是只对失败状态进行处理,没有对成功状态进行处理。那么新任务的状态和相关数据,就跟旧任务一致
- 有后续处理,但是还没有执行,新任务状态为挂起。代码如下
const buy = new Promise((resolve) => {
setTimeout(() => {
resolve("买到了");
}, 2000)
})
const result = buy.catch(() => {
console.log("没买到")
});
setTimeout(() => {
console.log(result); // Promise { <pending> }
})
可以看到输出的状态为挂起,因为旧任务还没有结果,新任务就得等着。
- 如果有后续处理并且执行了,就会根据后续处理的情况决定新任务的状态
- 后续处理代码没有报错新任务状态为完成,相关数据为
return
的返回值 - 后续处理代码报错新任务状态为失败,相关数据为错误对象
- 后续处理的是
return new Promise()
,返回一个新对象,新任务状态跟这个对象的状态一致
- 后续处理代码没有报错新任务状态为完成,相关数据为
- 先看第一个情况后续处理没有报错,比如说买可乐买到手了,后续操作是要打开拉环
const buy = new Promise((resolve) => {
resolve();
})
const result = buy.then(() => {
console.log("买到了");
return "成功打开拉环";
});
setTimeout(() => {
console.log(result); // Promise { '成功打开拉环' }
})
// ----------------------------------------------------------------------
// 新的情况,你去买可乐,但是付钱了没有买到,你又重新付了一次钱。这个重新付一次钱是新的任务,代码如下
const buy2 = new Promise((resolve, reject) => {
reject("没买到");
})
const result2 = buy2.catch(() => {
console.log("重新付钱,购买成功");
return "成功";
})
setTimeout(() => {
console.log(result2); // Promise { '成功' }
})
上面代码可以发现,只要后续处理没有报错,新任务的状态就一定会是成功。
- 接下来是后续代码出错,也是买可乐也买到,但是你发现这个可乐易拉罐没有拉环!!!。
const buy = new Promise((resolve) => {
resolve();
})
const result = buy.then(() => {
// throw new Error("什么鬼,居然没有拉环!!!")
throw "什么鬼,居然没有拉环!!!";
});
result.catch(() => {}); // 得加一个后续处理不然会弹出警告,不用管这行代码
setTimeout(() => {
console.log(result); // Promise { <rejected> '什么鬼,居然没有拉环!!!' }
})
看上面代码你就会发现,抛出一个错误后,状态变成了rejected
。你可以手动抛出错误,代码书写错误报错一样会导致任务失败
- 最后一种情况是后续处理里面
return new Promise(())
const buy = new Promise((resolve) => {
resolve();
})
const result = buy.then(() => {
return new Promise((resolve, reject) => {});
});
setTimeout(() => {
console.log(result); // Promise { <pending> }
})
上面第一步将状态改为成功了,但是新任务里面返回了一个新任务对象,这个时候新任务的状态就要取决于这个新对象的状态。
使用链式调用优化下载电影功能
function download(name) {
return new Promise((resolve, reject) => {
// 模拟正在下载
console.log(`正在下载《${name}》......`);
// 模拟一秒钟后下载成功或是失败
setTimeout(() => {
if (Math.random() < 0.8) {
resolve(`下载《${name}》成功`);
} else {
reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
}
}, 2000)
})
}
// 调用then方法连续为成功状态做后续处理,在最后处理失败状态
download("电影1").then(data => {
console.log(data);
return download("电影2");
}).then(data => {
console.log(data);
return download("电影3");
}).then(data => {
console.log(data);
return download("电影4");
}).then(data => {
console.log(data);
console.log("全部下载完成")
}, reason => {
console.log(reason);
})
其他API
方法 | 作用 |
---|---|
Promise.resolve() | 直接返回一个状态为fulfilled 的promise对象 |
Promise.reject() | 直接返回一个状态为reject 的promise对象 |
Promise.all() | 返回一个promise对象,传入一个任务数组,所有的任务为成功这个对象就成功,有一个失败这个对象的状态就为失败 |
Promise.any() | 返回一个promise对象,传入一个任务数组,有一个成功即为成功,所有任务失败这个对象就失败 |
Promise.allSettled() | 返回一个promise对象,传入一个任务数组,所有任务的状态从未决阶段 到已决阶段 ,这个对象就成功,不然这个对象就是pending
|
Promise.race() | 传入一个数组,返回第一个变为已决阶段 的任务 |
async和await
概述
有了Promise异步任务有了一种统一的处理方式,有了统一的方法ES官方进行了进一步的优化,ES2016提出了两个关键字async
和await
,用于更好的表示一个promise
async
这个关键字用于修饰一个函数,但凡是使用async
修饰的函数那么这个函数一定会返回一个promise。如果返回值不是Promise就会被隐式包装成Promise
如果函数中的代码没有报错,返回的就是成功状态的promise,相关数据就是return的值。
async function demo() {
return "demo";
}
demo(); // Promise { 'demo' }
// 上面的写法香相当于
function demo1() {
return new Promise(resolve => {
resolve("demo");
})
}
// 当然也可以标记立即执行函数
(async function () {}())
(async () => {})()
有错误则返回一个失败状态的promise,相关数据就是报错的信息。如下
async function demo() {
throw new Error("代码出错了");
}
demo(); //Promise {<rejected> Error: 代码出错了}
如果一个被async
标记的函数里面return new Promise()
的话,照上面的逻辑,函数是不是应该返回一个Promise然后相关的数据是这个函数里面return
的Promise,像这样Promise {Promise}
。但其实不然,当一个函数用async
标记了return
的是一个Promise那么这个async
就跟没有写一样,如下。
async function demo() {
return Promise.resolve("demo");
}
demo(); // Promise {"demo"}
// 等价于
function demo() {
return Promise.resolve("demo");
}
demo(); // Promise {"demo"}
await
这个关键字表示等待某一个Promise完成,必须在async
函数中使用。完成之后会返回这个Promise的相关数据。如下
function buy() {
return new Promise(resolve => {
resolve(10);
})
}
async function demo() {
const result = await buy();
console.log(result);
}
// 等价于
buy().then(data => console.log(data));
如果没有相关数据也可以写成下面这样
function buy() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000)
})
}
async function demo() {
await buy();
console.log("执行完成")
}
demo(); // 执行完成
一个Promise任务可能会有成功,也可能有失败,那么怎么处理失败呢,失败await他会抛出一个错误,可以使用try...catch
,如下。
function buy() {
return new Promise((resolve, reject) => {
if(Math.random() < 0.5) {
resolve("成功");
}else {
reject("失败");
}
})
}
// 使用try...catch的方式处理成功或失败。
async function demo() {
try{
const result = await buy();
console.log(result);
}catch (error) {
console.log(error);
}
}
demo();
await
不止可以以等待Promise,还可以等待其他数据,但是他会隐式封装成Promise。如下
async function demo() {
const n = await 10;
console.log(10);
}
demo(); // 10
// await 10 等价于 await Promise.resolve(10)
下载电影的完美调用方法
function download(name) {
return new Promise((resolve, reject) => {
// 模拟正在下载
console.log(`正在下载《${name}》......`);
// 模拟一秒钟后下载成功或是失败
setTimeout(() => {
if (Math.random() < 0.9) {
resolve(`下载《${name}》成功\n`);
} else {
reject(`下载《${name}》失败 --> 网络错误,所有任务暂停`);
}
}, 2000)
})
}
// 需要下载的电影
let films = [
'电影1', '电影2', '电影3', '电影4',
'电影5', '电影6', '电影7', '电影8',
'电影9', '电影10', '电影11', '电影12',
'电影13', '电影14', '电影15', '电影16',
'电影17', '电影18', '电影19', '电影20',
'电影21', '电影22', '电影23', '电影24',
'电影25', '电影26', '电影27', '电影28',
'电影29', '电影30', '电影31', '电影32',
'电影33', '电影34', '电影35', '电影36',
'电影37', '电影38', '电影39', '电影40'
];
// 使用async标记立即执行函数,然后里面使用循环配合await
(async () => {
for (let i = 0; i < films.length; i++) {
try {
const result = await download(films[i]);
console.log(result);
} catch (data) {
console.log(data);
break;
}
}
})()
上一篇: layui的按钮禁用与启用
下一篇: 视频缓存加速,2.5倍优化