作为前端开发人员,对于promise应该都是不陌生,基本上都有过,new promise, 然后.then, 原型里也提供了一些方案,race, all等等。。。对于解决同步的问题可谓是十分方便。然而,前段时间面试:
“知道promise吗”,
“知道”,
“那你说说什么是promise”,
“promise就是解决异步操作的一种方案,避免的函数回调”,
“嗯,那还有呢,具体怎么实现的呢”
“emmmm…”
“ok,那你知道async么,这个和promise区别是什么呢?”
…真的是一时语塞,当时紧张的随便说了两句,现在都忘了自己说了些什么。原来平时真的是专注于怎么方便怎么来,本着会用就行了的心态果然很容易把自己的路走窄呀。
所以,还是乖乖关注其方法本身吧。
#####为什么要用promise呢?
答案很明显,promise是为了解决异步的可操控性。node.js是单线程异步执行环境的。如果两异步接口存在依赖关系,比如A接口需要依赖B接口,那么我们希望A接口在B接口之前执行。
我们也许用如下的方式去控制方法的执行顺序
B接口里需要执行ajax请求,在B方法中定义一个变量val来监听ajax请求是否返回正确的值,再通过val来判断是否需要调用A方法。(以下代码为简写,理顺思路就好)
function A(){
console.log('----输出A----')
}
function B(val){
if (val < 5) {
A();
}
}
B(4);
或者是通过setTimeOut来控制方法的执顺序。
function A(){
console.log('----输出A----')
}
function B(val){
console.log('---val----'+val);
setTimeout(()=>{
A();
},5000)
}
B(4);
通过函数回调的方式如果是非常简单的操作,这样的写法也未尝不可。
但是如果是很复杂的逻辑,也许过了十天自己都回忆不起来,这是个啥。。
另外,这样写的坏处,就真的是造成了代码冗余,而且不能复用,并且让你觉得写代码是件繁琐的事情。
函数回调,很可能遇见信任问题,what?
1.调用回调过早(在它开始追踪之前)
2.调用回调过晚(或者不调)
3.调用回调太少或者太多次
4.吞掉了可能发生的错误/异常
5.没有传递正确的参数(还是有可能的,例如接口相应超时,超出了等待时间)
…
总之使用回调的方式,确实会出现很多让人难受又很奇葩的错误。
#####首先会用。ok,会用的程度是什么样子的呢?
new Promise(function(resolve, reject){
console.log('---执行步骤一,输出,成功---')
resolve();
}).then(function(resolve,reject){
console.log('---执行步骤二,输出,失败----')
reject();
}).catch(function(resolve, reject){
console.log('----最后一步失败---')
})
输出
—执行步骤一,输出,成功—
—执行步骤二,输出,失败----
----最后一步失败—
甚至,我们在vue/react中,使用axios,fetch,dispatch等请求数据也会去支持promise API,非常直观的表达了函数的调用顺序。比如说:
dispatch({
type: 'xxx/xx',
params:{
content: 'test1'
}
}).then(res => {
if (res) {
...doSomeThing...
}
})
以上,我想大部分都用到这个程度吧。
#####来剖析剖析promise
如果你来实现一个promise函数呢?应该怎么写?我们沿着这个思路,写一个promise的实现方案,顺便来了解了解它的思想。
我们以生活中的例子:
比如有一天我们需要外出办一件很紧急的事情,但是朋友又要来家里聚餐。最理想的状态就是,当我办完紧急的事情回来后,可以和朋友们聚在一起,吃着美食。那么promise在这时候充当的角色就是我的小助手mm,她需要为我做好一顿美食。接下来,我需要列好任务,告诉她如何才能做好这顿美食。以下就是我的任务清单。
1.去菜市场买菜
2.将买回来的菜熬成汤,做成好吃的东西
3.打电话告诉我的朋友通知他们过来聚餐
4.通知我回家
1,2,3步骤我们就理解为异步的执行过程吧,但是我要告诉mm我希望的过程。那么将上面的话用promise的写法,就可以翻译成
// 告诉mm帮我做几件连贯的事情,先去菜市场买菜
new Promise(买菜)
//用买好的菜做饭
.then((买好的菜)=>{
return new Promise(做饭);
})
//打电话告诉朋友们饭好了
.then((做好的饭)=>{
return new Promise(饭好了);
})
//通知完朋友打电话通知我
.then((通知朋友玩了)=>{
电话通知我();
})
promise是承若我一定帮你做一件事情,并且会告诉你事情的结果。
那么promise的核心还在于它的状态机制,一共有三个状态。
pending: 异步正在进行中
resolved: 执行成功
reject: 执行失败
那么按照状态,则是pendding => resolved => 返回结果
=> reject => 返回错误结果
也就是说,执行成功后,pedding状态变为resolved,执行失败后,pedding状态变为reject。这个状态一旦发生改变了,就不会再变回去了。
#####那就开始实现吧
这里就简单说说思路,具体时间这里整理时间也没有那么多了。
promise里面有then,resolve,reject这两个静态方法,也提供了all,race这些方法。
开始的第一步:
初始化promise实例,需要达到的目的:
1.改变promise中的status状态,将pedding状态变为resolved或者rejected。
2.当检测到是成功还是失败的状态时,返回相应的执行函数,传递出promise执行对象
(export default) class PromistMine{
constructor(executor){
// 状态,promise里面基本上就这种状态了
this.status = 'pedding';
this.value = undefined; // 返回失败的值
this.reason = undefined; // 失败成功原因
this.onRejectCallBacks = []; // 存放失败的回调,这个是成功的回调函数
this.onResolveCallBacks = []; // 存放成功的回调,这个是失败的回调函数
/**
* 处理的两种状态
**/
// 执行成功的状态
let resolve = (reason) => {
if (this.status === 'pedding') {
this.status = 'resolved';
// 把成功后要返回的信息透传
this.reason = reason;
this.onRejectCallBacks(fn => fn());
}
}
// 执行失败的状态
let reject = (data) => {
if (this.status === 'pedding') {
this.status = 'rejected';
this.value = data;
this.onRejectCallBacks(fn => fn())
}
}
// 在new Promise时,都需要传递两个参数,resolve,reject用于处理方法是否成功后的回调
try {
executor(resolve,reject);
} catch (e) {
reject(e) // 执行执行器,如果throw new Error('error') 就直接走reject了
}
}
}
开始的第二步:
实现resolve和reject这两个静态方法。
resolve和reject这两个方法本质上其实是一样的。这里是promise提供的两个静态方法,目的在于,状态是成功或者失败时,返回自身。
这两个静态方法其实和构造函数里的resolve与reject原理一样。
开始的第三步:
实现then的链式调用。
在平时使用promise,then字面意思是“下一步,接下来”。这个回调函数里,可以做的事情是:1.执行上一步的方法,并且在上一步的返回结果里执行一系列的操作。2.返回自身实例,记录执行结果,便于链式调用。
在原型上添加方法,new的时候直接继承了
PromistMine.prototype.then((onFulfilled, onRejected) => {
// 因为从原型上新增的方法,所以可以拿到this作用域
if (this.status === 'pendding') {
// 将报错或者成功的信息存起来
this.onRejectCallBacks = this.onRejectCallBacks.push(onRejected);
this.onResolveCallBacks = this.onResolveCallBacks.push(onFulfilled)
} else if (this.status === FULFILLED){
onFulfilled(value);
} else {
onRejected(this.reason);
}
return this; // 返回其本身
})
大概的思路就是这些了。能看到这已经很不容易了。
记录一下常用race,all的使用
先说说all吧。
promise的all方法,在官方的类型是这样去描述这个方法的。它允许多个promise对象以数组的形式传入。
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
promise提供的all方法,有一种共存亡的概念。当数组内所有的promise都被接受了,那么执行成功的回调;如果传入的promise数组中有一个被拒绝了,那么就执行失败后的回调;如果存在多个promise被拒绝了,返回的信息将是第一个promise被拒绝的消息。
for example,其他的状况就不一一列举了:
const demo1 = new Promise((resolve, reject)=>{
reject('接受到')
});
const demo2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('接收到了')
},122)
})
Promise.all([demo1,demo2]).then(resolve=>{
console.log(resolve)
}, reject=>{
console.log(reject)
})
output: 接受到
promise提供的race方法,race按表面的意思可以知道是赛跑的意思。race的入参和all方法是一样。允许传入多个promise对象。但是race更追求的是速度,不管是接受状态还是拒绝状态,谁快谁先输出。
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 110, 'one');
});
var promise2 = new Promise(function(resolve, reject) {
setTimeout(reject, 100, 'two');
});
Promise.race([promise1, promise2]).then(function(value) {
console.log(value);
// Both resolve, but promise2 is faster
});
在前面提到,在promise中,有三种状态,pending,resolved,rejected。但在pedding变为resolved或者rejected时,这个状态我们叫决议状态。
在静态方法reject,resolve这两个方法,可以改变返回的状态,reject,resolve不会直接传参,而是将参数当做拒绝或者接受原因拒绝promise。
在resolve方法中,传入一个reject,此时返回的也是一个reject,被拒绝的promise。
例如
new Promise((resolve, reject)=>{
resolve(Promise.reject('---解决了---'));
}).then((resolve)=>{
console.log('----成功解决---');
console.log(resolve)
},(reject)=>{
console.log('-----失败解决---');
console.log(reject);
})
> -----失败解决---
> ---解决了---
记录一下promise的then
promise提供了then的方法,方便函数的回调。而前面我们也提到,then它自身的意义在于记录执行结果,返回其本身,这样就可实现链式调用了。
promise在平时用到最多的场景,调用接口后再执行其他的操作。在我刚刚接触promise时,总会写出让人惊奇的代码,找错误找了半天,最主要的就是平时对promise的理解还不够。
让人惊奇代码之一:
new Promise((resolve,reject)=>{
console.log('---执行步骤1---');
}).then(()=>{
console.log('---执行步骤2---');
}).then(()=>{
console.log('---执行步骤3---');
})
---执行步骤1---
这里的promise并没有执行resolve,所以promise一直在一个决议的状态,也没有返回其自身的实例,所以then里面的方法不执行那是当然的。
让人惊奇代码之二:
const promise = new Promise((resolve, reject) => {
resolve('完成');
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
> first messaeg: 完成
> second messaeg: undefined
这里一直取不到值,又是为什么呢?
悄悄的改一下代码
const promise = new Promise((resolve, reject) => {
resolve('完成');
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
return 'new'+msg;
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
> first messaeg: 完成
> second messaeg: new完成
为什么呢?因为第二个then里,接受上一个return的返回值,也就是新的promise是根据上一个函数的返回来进行决议,但是上一个then并没有return值,默认值为undefined,所以第一段代码会return一个undefined。
/------------------------------------------一年后我又回来了-----------------------------------------------/
补录上前段时间遇见的场景:(循环下的promise)
请求A,其返回类型为[{id:1, name: 2},{id:2, name:3}]这样的list集合
请求B为后来加上的接口,其返回值为请求A下的每一项更详细的数据
需求:将请求B的数据加载到A的结果中返回。
咋一看,几个for循环不就搞定的事情嘛。其实不是。
框架的技术栈是react,在render阶段中,dom树会不断的进行新旧对比,而不断的去更新dom结构,如果在render中去调用接口,那么会发起很多不必要的请求。(可以在componentDidMount这个状态中去调取接口,但是两个接口是异步请求,且在for循环赋值时,因为事务造成代码执行顺序不一样而无法及时拿到更新后的值)
我要的结果,A先执行完再执行B,遍历A,将B的数据插入,再传递给dom。这样既不会调用多余的接口,进行一遍for循环就ok,我的写法如下:
const promiseA = new Promise((resolve, reject) => {
dispatch({
type: 'xxx/getDomainDeployInfo',
payload: this.params,
}).then((res) => {
if (res && res.resultObj) {
resolve(res.resultObj);
} else {
reject();
}
})
});
promiseA.then((res) => {
let promiseList = [];
let promise = Promise.resolve();
res.forEach((result) => {
const {xxx, batchNumber} = result;
if ( xxxx&& batchNumber == 1) {
const params = {
xx1,
xx2
}
promise = promise.then(() => {
return new Promise((resolve) => {
dispatch({
type: 'xxxxx/fetchXX,
payload: params,
}).then((res1) => {
if (res1.success && res1.resultObj){
const paramsOne = {
...params,
...res1.resultObj
};
promiseList.push(paramsOne);
resolve(paramsOne);
} else {
resolve();
}
})
})
})
}
})
promise.then((value)=>{
let list = res;
if (res && res.length > 0 && value ) {
list = res.map((info) => {
if (info.domain == value.domain) {
const { c= {}, v= {}} = value;
return {...info, c, v};
} else {
return info;
}
})
}
_this.setState({
SSSS: list,
})
})
})