关于Promise的基本内容,已经写过一篇文章。基本的会用,偶尔也用过,不过知识点比较多,并且代码在外面套了好几层,感觉也比较复杂,一直理不顺头绪。最近看了下面链接的几篇文章,感觉很不错,对于Promise基本概念的理解比以前要清晰一点了。
Promise的知识点很多,这里是一次从多到少的收敛过程,写了几点平时可能会用到的最基础的用法。
大白话讲解Promise(一)
这篇文章写得还是比较简单直接的,对于理解概念很有帮助,推荐好好看看。
廖雪峰的Promise
这篇文章对于概念的分解还是比较详细的,很不错。里面的例子直接copy到chrome的控制台会报错,不过简单修改一下就可以了。本文的例子基本上都是从这里简单修改来的。
适用Promise的三种场景?
-
场景1: 级联调用,就是几个调用依次发生的场景。这个就是有名的回调地狱。通用的套路是,新建一个Promise,启动流程,其他的Promise,放在级联的then函数中。
-
场景2:几个异步回调都成功,然后再进行下一步操作,比如图片比较大,分成几个小图下载,然后拼接,就是一个典型的场景。这里用Promise.all这个函数就很方便。
-
场景3: 几个异步回调,只要有一个成功,就可以进行下一步。比如像主站和另外两个备用站点同时请求一张图片,只要有一个有相应,其他几个就可以放弃。这里用Promise.race这个函数就很方便。
Promise和回调地狱是什么关系?
-
单个的回调函数不会形成回调地狱。串行的回调,也就是上面提到的场景1,层层嵌套,导致代码越套越深,结构复杂,才形成了回调地狱。
-
Promise并不是替代回调函数,而是对回调函数的一层封装。比如,有名的setTimeOut函数,Promise并不能替代它,只是对它进行了一层包装。
-
当然,也不是简单的包装,最本质的变化,就是将对回调函数的调用,修改成了消息发送。将对结果的处理剥离出去,交给后续的对象处理。
resolve(data); reject(error);
这两个理解为消息发送函数更确切一点。一方面,将所处的Promise
的状态由pending
改为resolved
或者reject
。另一方面,生成一个新的Promise
,并返回,同时将数据作为参数传递出去。 -
Promise
包装的函数,同步和异步都是可以的,没有本质区别,按照一套思路去理解就可以了。同步的代码基本不需要包装,本来就简单,比如用Promise.resolve(初始值)
发起一个流程。大多数情况,Promise
包装的都是异步对象。本质是为了把层层嵌套异步回调代码,回调地狱 callback hell,转变为串行的链式调用。 -
Promise
对象新建的时候,状态是Pending
,可以理解为“正在进行中,需要等待”。
resolve(data);
一下,状态变成了Resolved
,链式调用的控制权就转移到了下一级的then(data => {callback(data)})
函数中。这里就是传统的回调函数执行的地方。
reject(error);
一下,状态变成了Rejected
,链式调用的控制权就转移到了catch(error => { })
函数中,一般建议放在最后面。这里就是集中处理错误的地方。
所以,不要纠结同步还是异步,将重点放在Promise
的对象的状态以及链式调用的控制权的转移上面。
Promise的编程范式:面向对象 or 函数式?
-
创建Promise对象,需要用到new关键字,并且名字也一般叫做对象。不过,Promise并不是面向对象的编程,更多的还是函数式。范畴或者集合,用类来模拟。如果能够提供一个静态函数Promise.of来替代new关键字,函数式的味道就更浓厚一点。
-
链式调用,比较方便,要做到这一点,每个函数,比如then,catch等,都返回Promise对象,叫范畴或者集合更确切一点。不过,每一个Promise都是不同的,这个符合函数式编程的习惯:生成新的对象,而不是改变对象本身。之间的联系主要是数据的传递,自身内部状态的变化。
Promise对异步调用的常用封装套路:
function promiseFunction(resolve, reject) {
let timeOut = Math.random() * 2;
let flag = (timeOut < 1);
let callback = () => {
if (flag) {
console.log('success...');
return resolve('data: 200 OK'); // 加个return是个好习惯
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加个return是个好习惯
}
};
// start process
console.log('start async function ...');
setTimeout(callback, (timeOut * 1000));
}
function asyncFunction () {
let promise = new Promise(promiseFunction);
return promise;
}
-
对callback的改造:
一般的callback,应该定义对结果的处理过程以及出错的处理过程。Promise剥离了这些具体的处理过程,改成了发消息。成功就发送resolve(data);
失败就发送reject(error);
这里要注意的一点是,resolve(data);reject(error);
,这两个消息函数至少要用一个,当然多用是没关系的,否则流程就启动不了。
resolve(data);reject(error);
之后,流程就交给后面的then或者catch来处理了,这之后的代码都不会执行。所以resolve(data);reject(error);
前面加个return,可以更加明确这种意图,是个好习惯 -
对流程函数的封装:
一般的异步过程都分为两步:在主线程发起异步过程,然后主线程就去做其他事情了;具体的工作一般在工作者线程中执行。工作完成后,调用callback,通知主线程,让主线程拿着结果做想做的事。
Promise把发起异步过程,(这里用setTimeou
t函数模拟),这个步骤封装在一个函数中,(就是Promise构造函数的executor参数),这个函数格式固定,参数是resolve, reject
,这里用一个名字promiseFunction把他列出来。 -
函数式编程范式的封装:
函数式编程一般会简化为范畴或者集合的操作,数据和函数都包裹在一个集合容器中。
这里用Promise类的对象来模拟,这也是导致误认为面向对象编程的原因。以固定套路的函数(resolve, reject)作为参数,通过Promise构造函数,用new关键字,得到了一个promise对象,完成封装。
所以,Promise对象在构建过程中,异步流程就已经发起了,Promise对象的状态就是pending===这个也是参考文章大白话讲解Promise(一)中提到的注意点
如果不resolve或者reject一下,(throw error跟reject是同一个意思),Promise对象就一直pending,这个链式调用就一直停着,动不了。 -
接口函数的封装:
这层封装是从软件工程的角度,方便使用者使用的角度来做的。
函数式编程用来完成跟界面和业务无关的具体功能是比较好的,操作的也是集合。但是一般来说,业务层用面向对象的模式进行设计的,调用函数式编程的集合不是很方便。所以,封装成功能型的函数,(也就是上面的asyncFunction函数),用起来就比较顺手了。
当然,把生成的Promise对象return出去,是为了方便链式调用。
在实际使用中,可以写得简洁一些,上面的代码可以精简如下:
function asyncFunction () {
return new Promise(function(resolve, reject) {
let timeOut = Math.random() * 2;
let flag = (timeOut < 1);
// start process
console.log('start async function ...');
setTimeout(() => {
if (flag) {
console.log('success...');
return resolve('data: 200 OK'); // 加个return是个好习惯
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加个return是个好习惯
}
}, (timeOut * 1000));
});
}
Promise简单使用的套路:
-
所谓简单使用,就考虑最简单的异步调用,(a)发起流程,等结果;(b)成功,处理结果;(c)失败,报错
-
Promise.prototype.then(callback)就是用来处理成功结果的回调函数,具体的处理过程在这里定义。
then函数的第二个参数可以用来处理出错结果,不过一般都不用。在这里处理错误是一种很差的方法。
then函数会返回一个Promise对象。这个前面已经提过,这个Promise对象是then函数内部新建的,和流程发起的那个Promise对象是不一样的。 -
then
函数一般建议写同步过程,这里是执行以往回调函数功能的地方。在流程最后,把接收到的data
再return
回去是个好习惯,万一后面还有其他的then
要用,数据data
就可以顺着节点传一下,不至于中断。
return data; 和 return Promise.resolve(data);
是等价的,内部估计会装换一下。所以本质上还是return
了一个Promise
对象
如果是异步过程,建议新建一个Promise
对象包装一下,再return
,这样就形成了串行依赖关系。
如果什么都不return
,那么内部会新建一个没有值的Promise对象,相当于return Promise.resolve(undefined);
;所以这种情况,链式调用还可以继续,但是参数传递会中断。
then函数中一般不建议放异步过程,这样做会增加理解的难度。下面这篇文章中就有这样的例子:
Promise.prototype.then()
-
Promise.prototype.catch()
本质上是.then(null, rejection)
的别名,这里是集中处理错误的地方,一般放在链式调用所有then的后面。这样可以捕获流程中的所有错误,包括主流程的以及后续then中出现的错误。 -
再简单的过程,(一般是异步过程,同步过程也一样,比如直通),也用函数包一下,(对外的接口统一为函数,把Promise对象隐藏起来),至少给一个then,最后跟一个catch。将原来一个整体的异步调用(流程发起,成功,失败)转化成了3级的链式调用,代码结构清晰很多。
比如上面通过Promise封装好的异步函数,典型的使用套路如下:
asyncFunction().then((data) => {
console.log(data);
return data; // 把数据往下传,是个好习惯
}).catch((error) => {
console.log(error);
});
场景1: 级联调用使用的套路:
-
这就是著名的回调地狱,callback hell,采用Promise包装之后,可以改为简洁的链式调用。其实就是用级联的then来体现这种级联的调用关系。
job1.then(job2).then(job3).catch(handleError);
其中,job1、job2和job3都是封装了Promise对象的函数
-
注意,这里的
job1、job2和job3
都要求是一个参数的函数。因为,不论是resolve,还是reject
,传值的参数个数都只有一个。这个可以联想到函数式编程中的柯里化,每次只传一个参数,简单直接。
如果想传个参数怎么办呢?将所有参数包装成一个对象就可以了,resolve和reject
都是可以传递对象的,只是个数规定为一个而已。不过运算的时候需要解析参数,再传值的时候需要重新组装参数,相对就麻烦一点了。 -
静态函数
Promise.resolve(data)
可以快捷地返回一个Promise
对象,一般可以用在链式调用的开头,提供初始值。 -
一般来说,在新建的
Promise
对象中发起异步流程,resolve(data)
消息发出之后,then(data => { callback(data) })
接收到数据data
,将原先的callback
在这里执行就好了。现在这里不放回调代码,而是return一个新的Promise对象,形成一个依赖链。
下面这个例子,就是先用Promise包装了一个异步过程,(乘10的函数);以及一个同步过程,(加100的函数);用随机数的方式,模拟过程失败的情况。然后通过then函数级联的方式定义依赖过程。最后用catch捕捉过程中遇到的错误。
Step1:用Promise封装过程
// input*10的计算结果; setTimeout模拟异步过程;
function multiply10(input) {
return new Promise(function (resolve, reject) {
let temp = Math.random() * 1.2;
let flag = (temp < 1);
console.log('calculating ' + input + ' x ' + 10 + '...');
setTimeout(() => {
if (flag) {
return resolve(input * 10);
} else {
return reject('multiply error:' + temp);
}
}, 500);
});
}
// input+100的计算结果;同步过程
function add100(input) {
return new Promise(function (resolve, reject) {
let temp = Math.random() * 1.2;
let flag = (temp < 1);
console.log('calculating ' + input + ' + ' + 100 + '...');
if (flag) {
return resolve(input + 100);
} else {
return reject('add error:' + temp);
}
});
}
Step2:用then函数级联的方式定义依赖过程:
// 结果是3300,或者报错
Promise.resolve(32).then(multiply10).then(multiply10).then(add100).then(data => {
console.log('Got value: ' + data);
return data;
}).catch(error => {
console.log(error);
});
// 结果是1160,或者报错
Promise.resolve(6).then(add100).then(multiply10).then(add100).then(data => {
console.log('Got value: ' + data);
return data;
}).catch(error => {
console.log(error);
});
// ... ... 还能写出很多的组合情况
-
一般情况
then(data => { callback(data) })
函数的主要工作是接收数据,然后执行原来的回调函数。
这里一般放同步代码;如果是异步代码,就像上面那样,可以新建一个Promise对象并返回,形成一个调用链。 -
如果既有同步的回调代码需要执行,又有异步的过程需要包装链接,怎么办呢?比如上面的例子,增加显示中间过程的功能。
可以考虑用两个级联的then函数分别来做这两件事。
一个then用来执行同步的回调函数。这里要注意将要传递的data return出去,不然,整个链式调用参数传递会中断。
.then(data => {
callbacek(data);
return data; // 这里要把接收到的data传出去,不然整个调用链的参数传递会断掉。
})
一个then用来包装异步过程的,并把这个新建的Promise return出去,形成异步过程依赖链。
.then(data => {
return new Promise(function(resolve, reject) {
let flag = ((Math.random() * 2) < 1); // demo flag
let newData = data + 1; // demo data
setTimeout(() => { // demo async function
if (flag) {
resolve(newData);
} else {
reject(new Error('error message'));
}
}, 10);
});
})
- 上面的新需求可以按照下面的套路简单实现:
// 结果是9880,或者报错
Promise.resolve(888).then(add100).then(data => {
console.log('add100之后的结果为:' + data);
return data;
}).then(multiply10).then(data => {
console.log('multiply10之后的结果为:' + data);
return data;
}).then(data => {
console.log('Got value: ' + data);
return data; // 这里是最后了,不return data对流程没影响。不过谁知道以后会不会加新的节点,return一下还是好的。
}).catch(error => {
console.log(error);
});
场景2: 几个异步回调都成功,然后再进行下一步操作
场景3: 几个异步回调,只要有一个成功,就可以进行下一步
-
这两种的实现方式很类似,可以按照一种套路模式
-
只考虑异步过程,不考虑同步过程
-
这里用到了两个静态函数,分别是
Promise.all()
,(场景2);Promise.race()
,(场景3); -
这两个函数的参数都是一个数组,数组的成员是
Promise
对象。 -
后面跟一个
then和catch
,就像是普通的使用场景。 -
Promise.all()
成功时,传递过来的是一个结果数组;失败时,传递过来的是出错对应的值。 -
这里没有链式调用,一长串的数据传递,所以这里的函数的参数个数没有限制。不过,统一为一个是比较好的习惯。就算没有参数,给个空对象也可以,万一以后要传呢
-
大白话讲解Promise(一)
Promise.all(),「谁跑的慢,以谁为准执行回调」;
Promise.race(),「谁跑的快,以谁为准执行回调」;
这个表述还是形象而准确的。
function asyncFunction1(data = null) {
return new Promise(function(resolve, reject) {
let temp = Math.random() * 2;
let flag = (temp < 1);
// start process
console.log('start asyncfunction1 ...');
setTimeout(() => {
if (flag) {
if (data) {
return resolve(data);
} else {
return resolve('success: asyncfunction1===');
}
} else {
return reject(`fail:asyncfunction1; temp:${temp}`);
}
}, 500);
});
}
function asyncFunction2(data = null) {
return new Promise(function(resolve, reject) {
let temp = Math.random() * 2;
let flag = (temp < 1);
// start process
console.log('start asyncfunction2 ...');
setTimeout(() => {
if (flag) {
if (!data) {
return resolve(data);
} else {
return resolve('success: asyncfunction2');
}
} else {
return reject(`fail:asyncfunction2; temp:${temp}`);
}
}, 500);
});
}
// 这里传过来的是成功结果的数组
Promise.all([asyncFunction1(), asyncFunction2()]).then(array => {
console.log(JSON.stringify(array));
return array; // 这里传递的是数组,比较特殊
}).catch(error => {
console.log(error);
});
// 结果是success: asyncfunction1;跑得比较快
Promise.race([asyncFunction1(), asyncFunction2()]).then(data => {
console.log(data);
return data;
}).catch(error => {
console.log(error);
});
done、finally、success、fail等其他内容呢?
-
这些一些框架提供的便利方法,当然,如果有需要,也可以自己实现。
-
上面这些是基本的使用套路,简单直接。一个基础应用加三个典型场景,可以应付平时大多数的异步过程。
-
当然,Promise还有很多高级而灵活的用法。下面推荐几篇文章,里面的内容很丰富。