文章目录
前言
本文章只是个人对Promise和Async知道的一些知识点的记录。有些说内容引用了阮一峰老师的es6入门文档。需要更加详细了解的还请移步到阮一峰老师的es6入门文档。
异步写法出现的原因
之前在哪里看来的原因之一是单线程阻塞,其实错误的(面试的时候面试官帮我指出了)。没有异步写法的时候,也可以有定时器回调写法、普通函数回调写法、阿贾克斯请求等实现异步。
解决回调地狱
第二,解决过去ajax请求多次出现回调地狱的情况。
ajax1(() => {
ajax2(() => {
ajax3(() => {
ajax4(() => {
ajax5(() => {
ajax6();
})
})
})
})
});
Promise对象
介绍
Promise 是异步编程的解决方案之一,它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,最终原生提供了Promise对象。
简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。不像其他监听函数,如果错过了,再去监听,是得不到结果的。
—《es6入门文档》
状态
Promise 有三种状态:
- 正在执行中
pending
- 执行成功
fulfilled
- 执行失败
rejected
名字缘由
Promise这个名字的由来,它的英语意思就是 “承诺” :
- 当前给不了结果,但承诺会在Promise内异步操作出结果后给你反馈。
- 只有Promise内异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
—《es6入门文档》
可以说是个非常负责的人。
使用例子
function PromiseFn() { // 用函数封装,因为new Promise就直接执行了
return new Promise((resolve, reject) => {
// 这里就写异步操作
setTimeout(()=>{
console.log("图片加载成功");
if (true) resolve("图片已经被添加了")
else reject("图片没有被添加")
},1000)
});
}
参数
可以看到Promise里面提供的两个参数:
resolve
函数,pending转成fulfilled做的事,即执行成功做的事;reject
函数,pending转成rejected做的事,即执行失败做的事;
注意:在写resolve()
和reject()
的时候要注意触发条件,不要异步操作还没做完就触发了。例如例子中的if条件。
then回调
Promise状态确定后,有个then()
回调,用来表述Promise执行结束后上面两个参数具体执行的事情。
即,第一个参数是执行成功后执行的resolve函数具体内容,第二个参数是执行失败后执行的reject函数具体执行内容(后面会用catch去代替它),例如:
function PromiseFn() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log("图片加载成功");
if (true) resolve("图片已经被添加了")
else reject("图片没有被添加")
},1000)
});
}
PromiseFn().then((res)=>{
console.log(res);
}, (rej)=>{
console.log(rej);
})
// 其中
resolve("图片已经被添加了")
// 为
("图片已经被添加了")=>{
console.log("图片已经被添加了");
}
reject("图片没有被添加")
// 为
("图片没有被添加")=>{
console.log("图片没有被添加");
}
好,理解上面之后,我们看看直接写一个Promise函数是怎么样的:
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('1s后执行了')
resolve('回调成功')
}, 1000)
}).then((res) => {
console.log(res)
})
前面说了只要状态确认了,就会执行then,所以我们还可以这样:
let resFn = Promise.resolve('1')
resFn.then((res) => {
console.log(res);
})
let rejFn = Promise.reject('2')
rejFn.then(null, rej => {
console.log(rej);
})
发现没, resolve
和reject
很有可能是个静态方法。
then的补充
默认返回一个Promise
then中的res和rej回调会默认返回一个Promise,并且传入参数undefined
:
let obj = new Promise((resolve, reject) => {
resolve('回调成功')
}).then((res) => {
console.log(res)
})
setTimeout(() => { console.log(obj) }, 0) // Promise {<resolved>: undefined}
所以如果在then后面再接一个then,那么后面这个then就会被调用:
PromiseFn()
.then(
(res) => {
console.log(res);
// 会默认返回一个带参数为undefined的Promise
},
(rej) => {
console.log(rej);
// 会默认返回一个带参数为undefined的Promise
}
)
.then(
(res) => {
console.log(res); // 上面的走res还是rej都会在这里接收到undefined
},
(rej) => {
console.log(rej);
}
);
那么可以在第一个then中返回一个Promise函数:
(res) => {
console.log(res);
return PromiseFn1()
}
那么就会正常在第二个then收到该Promise的返回值。
下一个then的状态确认
分几种情况
第一:如果后续没有处理,新的Promise状态和上一个保持一致,数据也为上一个的数据。也就是说上一个为成功,那么新的也为成功;上一个失败,新的也是失败。
const pro1 = new Promise((resolve, reject) => {
resolve()
})
const pro2 = pro1.then(null, () => { })
const pro3 = pro1.catch(() => { })
没有对成功做后续处理,所以pro2、pro3状态和pro1一致,为成功态。
const pro1 = new Promise((resolve, reject) => {
reject()
})
const pro2 = pro1.then(() => { })
没有对失败做后续处理,所以pro2状态和pro1一致,为失败态。
第二:上一个还处在pedding状态时,新的也是pedding状态
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
}, 3000)
})
const pro2 = pro1.then(() => { })
setTimeout(() => {
console.log(pro2);
}, 0)
第三:如果有对应的后续处理,后续处理中如果正常执行,则新的Promise为成功态,数据为手动return的值。后续处理如果有错误,则新的Promise为失败态,数据为手动return的值或者手动抛出错误。
这个很好理解就不举例了
第四:如果有对应的后续处理,且在后续处理中手动返回了新的Promise,则新的状态和数据与返回的新Promise保持一致。
const pro1 = new Promise((resolve, reject) => {
resolve()
})
const pro2 = pro1.then(() => {
return new Promise(() => { })
})
新返回的这个Promise状态没确认下来,所以是pedding,所以pro2也是pedding。
如何阻止失败状态后走下一个then
那如果第一个then中的走rej了,我们一般都不会让其继续走下一个then,可以这样写(这个可以不记):
PromiseFn()
.then(
(res) => {
console.log(res);
},
(rej) => {
console.log(rej);
return new Promise(()=>{}) // 失败就返回一个初始化状态的promise不让走下面的then
}
)
.then(
(res) => {
console.log(res);
},
(rej) => {
console.log(rej);
}
);
回调可以消参
还有一个是回调可以消参:
function fn(str) {
console.log("打印---", str);
}
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("回调成功");
}, 1000);
}).then((res) => { // 回调执行,传入参数
fn(res);
});
// 回调可以把参数消掉,会自动带入
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("回调成功");
}, 1000);
}).then(fn);
catch()
假如我们在Promise的回调中报错了,例如:
function PromiseFn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) resolve("执行resolve")
else reject("执行reject")
}, 1000)
});
}
PromiseFn().then(
(res) => {
console.log(obj) // 打印一个不存在的东西
console.log(res)
},
(rej) => {
console.log(rej)
}
)
那么打印台就会报错,此时可以用catch方法去捕获这个错误,而不报错:
PromiseFn().then(
(res) => {
console.log(obj) // 打印一个不存在的东西
console.log(res)
},
(rej) => {
console.log(rej)
}
).catch((e) => {
console.log('手动处理这个错误', e) // 这个e参数是不定的,只是大家约定俗成,他有name,message属性
})
然后,我们可以用catch去代替then的第二个函数的具体内容:
PromiseFn().then(
(res) => {
console.log(res)
}
).catch((rej) => {
console.log('既能捕获resolve的错误,又能代替执行reject', rej)
})
既能捕获resolve的错误,又能代替执行reject。 所以,以后都这样去执行一个Promise函数。
补充:在catch中和then是一样的,会默认返回一个入参为undefined的Promise,以下举例子:
// 1 promise直接走失败态,不抛出错误,p函数为成功态
const p = Promise.reject().catch(() => {
console.error('catch some error')
})
p.then(res=>{console.log('走成功状态', res)}) // 此时p是个resolved的Promise
// 2 promise直接走失败态,抛出错误,p函数为失败态
const p = Promise.reject().catch(() => {
throw new Error('err')
})
p.catch(err=>{console.log('走失败状态', err)}) // 此时p是个rejected的Promise
拓展:
Promise.prototype.catch
方法是.then(null, rejection)
或是
.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
结合then的执行问题
如果你在上面对then的理解很透彻的话,下面的题完全就没问题了
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
// 1 2
// 第四题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).then(() => {
console.log(2)
}).catch(() => {
console.log(3)
})
all()
可理解为并发执行多个Promise,使用例子:
let yibuFn = functions(n){
return new Promise(.......)
}
Promise.all([yibuFn(1),yibuFn(2),yibuFn(3)]
.then((res) =>{
// 全部成功后的回调
},(rej)=>{
// 全部失败后的回调
}
)
// 比较方便的写法是
let requests = [] // 用一个数组把所有的promise()请求一个一个的push进去
Promise.all(requests).then(values=>{
values.forEach(res=>{
... // 把结果循环出来使用
})
})
重点:
- all的入参数组要是Promise,并且是一个执行的状态
- 全部返回的数据是数组的形式,且会按照请求的顺序排列好!
all()也有缺点,在ajax请求中,如果这个Promise队列里出现了reject,例如其中一个接口报错。那么Promise.all()返回的结果会被一个reject而报销(其他正常返回也没用了)。
如果想捕获rejected,也可以使用catch,蛋疼的是只能捕获到第一个转变为rejected的Promise,不能捕捉所有发生rejected转变的Promise。
看起来也是个静态方法。
手写类似原理
这个题目真挺不错的,考验了你对Promise的理解程度,还对编码能力有一定的小要求。
function promiseAll(item) {
return new Promise(function (resolve, reject) {
let res = [] // 存储成功函数返回的值
let num = 0 // 记录都返回成功的数字
let len = item.length // 数组的长度
for (let i = 0; i < len; i++) {
item[i].then(function (data) {
res[i] = data
if (++num === len) {
resolve(res)
}
}, reject)
}
})
}
验证
function p(msg, delay = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(msg)
}, delay)
})
}
let arr = [p('1'), p('2', 3000), p('3', 2000)]
promiseAll(arr).then(res => {
console.log('promiseAll', res);
})
最终3s后打印[1, 2, 3]
这个手写题要注意的地方:
- 用数组下标的方式写入结果,保证结果的顺序性
- 用一个值类型计数,而不是让结果的数组长度与promise队列长度去比对,来决定是否全部完成。如果用后者去判断,会出现假设最后一个promise先完成,然后直接通过判断结束任务了。
race()
多个Promise执行,只回调第一个状态确定的:
Promise.race([p1, p2]) //这两个哪个先执行成功哪个就传入result
.then((result)=>{
console.log(result)
})
.catch((err)=>{
console.log(err)
})
}
应用:用all请求多个接口的过程中,当其中有个接口请求时间超过3s就不去管了,防止因为一个接口而拖累其他的接口:
function PromiseFn(delay) {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) resolve("执行resolve")
else reject("执行reject")
}, delay)
});
return Promise.race([p, new Promise(resolve => {
setTimeout(() => resolve(delay + 'timeout'), 2000)
})])
}
Promise.all([PromiseFn(1000), PromiseFn(3000)]).then((res) => {
console.log('res', res) // ['执行resolve', '3000timeout']
})
finally()
无论promsie执行成功还是失败,都会调用。
.then(function(){
console.log('success');
}).catch(function(){
console.log('catch');
}).finally(function(){
console.log('finally');
});
应用:可以把取消ajax加载动画放在这里处理
Async/await
诞生原因
只是我的猜测,因为使用Promise的回调会默认返回Promise,如果需要连续触发异步,写起来就变成一个链式写法,代码量比较多(但已经比回调地狱好很多了hhh):
let promise = aPromise() // 必须要有一个变量接收要不报错
.then(() => {
return aPromise();
})
.then(() => {
return "哦吼吼";
})
.then(() => {
console.log("上一步还能直接返回变量", res);
return aPromise();
})
.catch((err) => {
console.log(err);
});
链式调用本质也是回调函数,为了彻底的消灭回调的写法,出现了Async,用同步的写法代替异步。
并且写法上更加易读简洁。
Promise链式调用用现在眼光看可能比较落后,但是在当时可是解决了回调地狱的问题。
使用
只需记住两条:
- 在函数前加入async,声明这个函数内有异步操作;
- 在async内的异步操作前加await,意思就是后面的老哥是个Promise/async异步操作,要等他的状态确定了才能继续向下走;
async function fn(){
...
await // 一个promise/async函数
...
}
讲讲这个await的细节:
await执行顺序
async function fn(){
同步A
await B
同步C
}
fn()
await后面的函数会直接执行,不是说执行完同步AC后再去执行,然后要等B完成后才去执行C,C就是微任务;
await后面跟一个Promise函数,并且会自动拿到resolve
或reject
的参数,不需要用then去接参数,所以如果有参数,需要拿个常量来接收,例如const data = await B
例子:把异步执行变成同步
function promiseFn(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log('异步执行完毕')
resolve('芜湖')
},2000)
})
}
async function fn(){
...
const a = await promiseFn()
...
const b = await promiseFn()
...
}
你看,这样写的代码没有链式调用,没有回调,全都是同步的写法,非常简洁易读。
await可以被try catch捕捉
async function fn () {
try {
cosnt p = await new Promise((resolve, reject)=>{
setTimeout(()=>{
reject(new Error('错误'))
})
})
} catch(e) {
console.log('error', e.message)
}
}
当await里的Promise报错时,catch能捕捉到错误。完全不受调用栈的逻辑影响。
所以可以用在很多场景中比如:
async function fn () {
try {
await aPromiseFn(1)
await aPromiseFn(2)
await aPromiseFn(3)
} catch(e) {
return console.log(e.round) // 当某个抛出rejected状态时,就捕捉到,下面的代码也不执行了
}
...
}
// 如果想并行执行
async function fn () {
try {
await Promise.all([aPromiseFn(1), aPromiseFn(2), aPromiseFn(3)])
} catch(e) {
return console.log(e.round) // 当某个抛出rejected状态时,就捕捉到,下面的代码也不执行了
}
...
}
到现在,其实已经能够看出async/await写法很强大灵活,还能把异步的写法写出同步的样子,大大提升易读性。
与Promise的关系
第一:其实async标记的函数内部自动会返回一个Promise,状态根据函数内部抛出的东西决定。
console.log(async function() {
return 4 // 此时打印的就是resolved状态的Promise
// throw new Error(4) 此时打印的就是rejected状态的Promise
// 当然也可以手写返回一个Promise:return new Promise(() => {})
})
例子:
// 1
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
// 2 所以async函数执行后可以加.then()
async function fn(){
await // 一个promise/async函数
if (true) {
return a
} else {
throw new Error('出错了');
}
}
fn().then((res)=>{
// 这个res就是return出来的东西,注意这里不是Promise的类似用法,只用来接收参数
}, (rej)=>{
// 捕获到的错误
})
第二:(代码来自慕课)
- await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
- await 后续跟非 Promise 对象:会直接返回
(async function () {
const p1 = new Promise(() => {}) // 已经执行了Promise状态已确认
await p1 // 这里的Promise在上一步已经状态确认
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2 // 这里可以看做是Promise .then()的写法
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err')
const res = await p3 // 因为reject状态不会走.then()所以不会执行
console.log(res) // 不会执行
})()
// 失败状态可以通过上面说的try catch语法来捕获
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
这里可以看看for of与await直接有什么知识点:【JS基础】流程控制,让逻辑产生分支
一些面试题
async function fn() {
return 100
}
(async function () {
const a = fn() // 获取到什么
const b = await fn() // 获取到什么
console.log(a)
console.log(b)
})()
(async function () {
const a = await 100
console.log(a)
const b = await Promise.resolve(200)
console.log(b)
const c = await Promise.reject(300)
console.log(c)
})()
直接调用aysnc函数
function promiseFn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('异步执行完毕')
resolve('芜湖')
}, 2000)
})
}
async function fn() {
await promiseFn()
console.log('fn执行完毕');
}
function fn1() {
fn()
console.log('fn1执行完毕');
}
fn1()
在fn1调用async函数fn,你会发现fn1先打印了,并不会因为函数fn是async函数而等待。除非改写成:
async function fn1() {
await fn()
console.log('fn1执行完毕');
}
循环等待
可以看看:【JS基础】流程控制,让逻辑产生分支(选择语句、循环语句)里的for of
和for await of
事件循环
Promise和Async经常用来考事件循环的题目,可以参考我这篇文章:【JS基础】克服事件循环机制的基础面试题,一点也不难
这里要注意一个问题,很多新人会犯的错误,Promise本身是同步的,他的回调才是异步的,具体还是看我上面的文章。
应用
vue中,一个Promise封装的接口请求,需要写成同步
getSomething(){
return ajax('get', '/user').then(res => {
return res
})
},
async dataDeal(){
// ...
let res = await this.getSomething()
// ...
}
封装一个获取某ui库form组件里的数据的方法:
// AsyncFn 这个方法就当做form校验api
function getData() {
AsyncFn().then(
(res) => {
console.log(res)
return res
}
).catch((e) => {
return e
console.log(e)
})
}
console.log('拿不到', getData()); // 这样是拿不到返回的res的,因为return的作用域在then函数里
// 改写
async function getData() {
let data = await AsyncFn().then(
(res) => {
console.log(res)
return res
}
).catch((e) => {
return e
console.log(e)
})
return data // 返回一个promise
}
(async function fn() {
console.log('拿到', await getData()); // 这样就可以拿到了
})()
并发请求一定数量的接口
这个是从掘金评论区看来的,写的很好:
// 模拟100个异步请求
const arr = [];
for (let i = 0; i < 100; i++) {
arr.push(() => new Promise((resolve) => {
setTimeout(() => {
console.log('done', i);
resolve();
}, 100 * i);
}));
};
const parallelRun = () => {
const runingTask = new Map(); // 记录正在发送的异步请求(闭包存储)
const inqueue = (totalTask, max) => { // 异步请求队列,每组请求的最大数量
// 当正在请求的任务数量小于每组请求的最大数量,并且还有任务未发起时,就推入请求
while (runingTask.size < max && totalTask.length) {
const newTask = totalTask.shift(); // 弹出新任务
const tempName = totalTask.length; // 以长度命名?
runingTask.set(tempName, newTask);
newTask().finally(() => {
runingTask.delete(tempName);
inqueue(totalTask, max); // 每次一个任务完成后就继续塞入新任务
});
}
}
return inqueue;
};
parallelRun()(arr, 6);