我们前端开发常见的问题,异步编程也是压在前端面试上的一座大山,无论是eventloop还是手写Promise,都是基础面试问题,今天我们就聊一聊Promise
1、讲个故事
今日李小明起床去内卷,路过某德基,吃个早餐吧,于是走了进去~
李小明:哈喽小姐姐,给我来一份汉堡、皮蛋瘦肉粥~
前台小姐姐: 好的,请扫码...这是您的订单号,我们预计5分钟准备好
于是李小明拿着订单,做了下来,开心刷起了抖音~
later......
前台小姐姐:9527你的套餐好了
李小明起身取了套餐,吃完后火速赶往地铁站
我们试着简单的流程图区梳理这个故事:
当然这是一个完美的故事,并不是每次都是这样,李小明同学去下单过程也会是平行世界的另一种可能~
前台小姐姐:不好意思今天皮蛋瘦肉粥没有了~
李小明心想两中都要才是早餐,那就不吃了,离开
那么我们用javascript程序的思维来去实现上面的逻辑,参考下面伪代码
function canEat(getHamburger, getPorridge, cb) {
let x, y
getHamburger(function (val) {
x = val
if (y != null) {
cb(`都准备好了:${x}+${y}`)
}
})
getPorridge(function (val) {
y = val
if (x != null) {
cb(`都准备好了:${x}+${y}`)
}
})
}
// 创建两个异步事件
function getHamburger(cb) {
// xxxxx
cb('十五层汉堡王')
}
function getPorridge(cb) {
// xxxxx
cb('皮蛋瘦肉粥')
}
canEat(getHamburger, getPorridge, (str) => {
console.log(str)
})
上面伪代码让我们通过canEat将多个异步任务集合起来,通过协调保证李小明同志能够同事吃到汉堡和皮蛋粥,当然这是一个粗糙的方式实现,那么我们再去用Promise的方式来去描述:
function canEat(getHamburger, getPorridge) {
console.log('我刷个抖音,摸摸鱼')
return Promise.all([ getHamburger, getPorridge ]).then((values) => {
return `${values[0]} + ${values[1]}`
})
}
// 创建两个异步事件
function getHamburger() {
// xxxxx
return Promise.resolve('十五层汉堡')
}
// 万一没有皮蛋瘦肉粥呢~
function getPorridge() {
return Promise.reject('皮蛋瘦肉粥')
// return Promise.resolve('皮蛋瘦肉粥')
}
canEat(getHamburger(), getPorridge()).then((value) => {
console.log(`都准备好了:${value}`)
})
.catch(e => {
console.log(`${e}没有了,我还吃个屁哦`)
})
这样我们就模拟了一个李小明同志吃早餐的Promise,看起来是不是很简单?等等,里面的Promise.All、Promise.reject等等这些又是什么鬼(抓狂ing~~),客官你先别着急,且听我慢慢道来~
2、Promise解决的问题
想一想我们在异步编程过程中遇到的问题需要考虑的无非是下面几个点:
- 1、调用回调过早或过晚
- 2、调用回调次数过多或过少
- 3、异步传递信息不对称
- 4、异步错误捕获
针对上面问题,我们看看Promise怎么解决这个编程信任危机
(1)调用回调过早或过晚
const p = new Promise((resolve, reject) => {
console.log("开始")
resolve('成功')
console.log('结束')
})
p.then(res => {
console.log(res, '拿到你给我的承诺~')
})
console.log(p)
/*
* 开始
* 结束
* Promise 对象
* 成功 拿到你给我的承诺~
*/
通过上面例子,我们看看Promise如何保证回调的执行的时机:
- 首先,Promise中的resolve、reject保证了其返回的信息,在同步任务队列中不会不获取到,只会在调用then()的时候会被获取;
- 其次,then()只会被异步任务队列获取,即使说我们直接resolve('something')也不会影响then()执行时序,这样就会保证回调内容不会过早的执行打乱主进程的节奏;
- 再者,then()注册的观察回调会监听resolvey与reject,只要这两个被调用了,那么then()机会进入到下一个异步事件的队列中被第一时间执行,这样就保证了只要resolve或者reject,我们总能在下一个异步中第一时间执行,避免结果返回,很晚才执行的尴尬等待时间;
(2)调用次数过多或过少
先让我们猜猜,李小明同学到底吃了什么套餐(无聊的题目又来了~),相信大家很容易猜出来~
const p = new Promise((resolve, reject) => {
resolve('汉堡 + 皮蛋瘦肉粥')
resolve('帕尼尼 + 煎蛋')
})
p.then((res) => {
console.log(`李小明的最终套餐${res}`)
}).then((res) => {
console.log(`我还能在混吃一份${res}`)
})
/*
* 李小明的最终套餐汉堡 + 皮蛋瘦肉粥
* 我还能在混吃一份undefined
*/
那么让我们捋一捋Promise究竟怎么保证决议与执行次数的~
- 首先:Promise的定义方式使得他只能被决议一次,不管在一个Promise内做了多少次resolve、reject都会以第一次出现的决议作为结果;
- 其次,当然有一次决议,那么我们的then()执行回调也就只会被执行一次,当然我们要说的是注册一次的then(),而非多次
p.then(); p.then()balabala,当然这种也会跟着注册次数而执行
(3)异步传递信息不对称
对于一个promise内只会有最先决议的内容会被当做内容作为值来传递下游,也就是上面提到的reject和resolve只有会有第一个生效的问题;如果没有任何显示决议值,就会以undefined值传递下去,不会存在传递错误的问题;
(4)异步错误捕获问题
在promise创建过程中,任何时间节点出现了javascript异常错误,都会终止掉当前的Promise,进入catch错误捕获阶段;正如下面一段代码
var p = new Promise((resolve, reject) => {
foo.bar() // 为声明foo
resolve(42)
})
p.catch(e => { console.log(e) }) // ReferenceError: foo is not defined
关于错误问题,下面我们讲一个面试套路题目,让你去了解一下错误捕获的问题
3、关于try ... catch 错误捕获的问题
同样我们举个三个例子一起来看看try...catch 错误捕获
// 第一个
try {
var p = new Promise((resolve, reject) => {
foo.bar() // 未声明foo
resolve(42)
})
} catch (e) {
console.log(e, '可以拿到')
}
// 第二个
try {
setTimeout(() => {
throw new Error('第二种错误')
}, 0)
} catch (e) {
console.log(e, '可以拿到')
}
// 第三个
async function errorFnc() {
try {
const a = await Promise.reject('第三种错误')
} catch (e) {
console.log(e, '可以拿到')
}
}
errorFnc()
copy每一份代码在浏览器控制台走一波,就能很容易的得到答案,那么让我们揭开谜底,只有第三个会进入到catch捕获阶段,其余两个都不会进入,why??
揭开谜底的时候,原理just so so
- 针对于try...catch是同步捕获,所以对于异步代码中错误无能为力(诸如定时器、reject等),可以很好的解释第二种;
- 针对第一种,我们就要理解Promise的错误处理机制,在Promise创建过程任何时间节点上的异常错误,都会被Promise的catch模块捕获,try...catch 想去拿到同步错误,然后被Promise的错误处理机制截胡了~
- 针对第三种,就是通过async await方式等待异步任务完成后继续执行,整体代码在try...catch代码块中是同步过程,所以能拿到错误处理,也是通过try...catch捕获Promise错误的一种方式
分析了这么多全是废话,总结后就一句:trycatch抓同步错误,promise错误自产自销;
4、关于Promise的Api
这块内容是很无聊,实际工作中,我们用到大多是下面几种
- new Promise((resolve, reject) => { xxxxxx }).then(res => { xxxx }).catch(e => { xxxx }) 第一种常规操作
- Promise.resolve() 和 Promise.reject(),这里我们可以通过Promise.resolve() 创建一个微任务对联,然后通过then的方式去执行一些统一任务,经典的便是,vue中一次性更新多个响应式数据,最后合并一次更新,便可以用这种方式来去模拟实现,这个可以参考我的另一个栏目“读《vuejs设计与实现》一书笔记”中的computed实现这章
- Promise.all() 与Promise.race(),Promise.all会常用一些,多个异步决定一个事件通常使用Promise.all
5、手写Promise
这块说实话,网上套路一大波,我这里没有比较在写(复)一(制)遍了,这个手写Promise有些卷王面试官喜欢问一波~~,所以高情商的办法就是跟着一起卷,写上一两遍就懂了,这块我推荐掘金一篇文章关于手写Promise的,链接在此;看这个之前还有个关于queueMicrotask建议先去看一遍,Mdn文档很详细,可以参考一波;
今天扯了扯Promise的一些非使用向的一些内容,如果内容对你们有所帮助,转评赞一键三连感谢各位看官的支持