【es6入门】好好捋一捋Promise与Async的异步写法,细节满满

前言

本文章只是个人对Promise和Async知道的一些知识点的记录。有些说内容引用了阮一峰老师的es6入门文档。需要更加详细了解的还请移步到阮一峰老师的es6入门文档。


异步写法出现的原因

之前在哪里看来的原因之一是单线程阻塞,其实错误的(面试的时候面试官帮我指出了)。没有异步写法的时候,也可以有定时器回调写法、普通函数回调写法、阿贾克斯请求等实现异步。

解决回调地狱

第二,解决过去ajax请求多次出现回调地狱的情况。

ajax1(() => {
  ajax2(() => {
    ajax3(() => {
      ajax4(() => {
        ajax5(() => {
          ajax6();
        })
      })
    })
  })
});

Promise对象

介绍

Promise 是异步编程的解决方案之一,它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,最终原生提供了Promise对象。

简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。不像其他监听函数,如果错过了,再去监听,是得不到结果的。
—《es6入门文档》

状态

Promise 有三种状态:

  1. 正在执行中 pending
  2. 执行成功 fulfilled
  3. 执行失败 rejected

名字缘由

Promise这个名字的由来,它的英语意思就是 “承诺” :

  1. 当前给不了结果,但承诺会在Promise内异步操作出结果后给你反馈。
  2. 只有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);
})

发现没, resolvereject很有可能是个静态方法。

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链式调用用现在眼光看可能比较落后,但是在当时可是解决了回调地狱的问题。

使用

只需记住两条:

  1. 在函数前加入async,声明这个函数内有异步操作;
  2. 在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函数,并且会自动拿到resolvereject的参数,不需要用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 offor 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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值