基本认识
Promise
提供了一种异步过程的不同写法。回调函数是层层嵌套的,而Promise
,类似于函数式的...链式调用。Promise
是一种容器,是对异步过程的一种封装。是对异步过程的一种新的写法。在此之前,异步过程一般都是通过回调函数的方式来写的。
同步过程也可以用
Promise
封装;不过纯粹是多此一举,没有必要。当然有时为了保持链式调用不中断,对同步过程也用Promise
包一下也是可以的。比如,Promise(10)。
对于多个异步过程级联的情况,在书写风格上,回调函数是一层层往里套,而
Promise
是横向链式调用。相对来讲,Promise
的链式调用更符合人的习惯,看上去更简洁。回调函数的实现比较简单,只要函数能够作为另外一个函数的参数就可以了。
Promise
实现相对比较复杂,以前的话一般要引入第三方的库。ES6
将其写进了语言标准,统一了用法,原生提供了Promise
对象。回调函数倾向于把异步过程的发起以及结果处理集中在一处;而
Promise
倾向于将异步过程的发起和结果处理分开来,处理结果通过消息传递。回调函数是一种集中式思维,对于简单的异步过程有优势。Promise
是一种分而治之的思想,对于相对复杂的链式调用比较有优势。回调函数是一种逆向思维。发起者定义结果处理函数,比如successCallback或者failCallback,以函数参数的方式传给异步处理函数。异步过程结束后,由被调用的异步处理函数来回调。
Promise
是一种正向思维。可以理解为一种消息发送。在构建Promise
对象的过程中自动发起异步过程。异步过程结束后,发出消息,后续的then
或者catch
接收消息,处理结果。
同步异步
- 同步:等待结果处理完成,然后返回。
function syncProcess() {
console.log('log1');
console.log('log2');
console.log('log3');
}
syncProcess();
// log1
// log2
// log3
- 异步:不会等待结果处理完成,直接返回;当有结果的时候,再来处理。
function asyncProcess() {
console.log('log1');
setTimeout(() => {
console.log('log2');
}, 0); // 异步,就算是0秒也不会等
console.log('log3');
}
asyncProcess();
// log1
// log3
// log2
级联的例子
需求:输入一个数,经过加2,乘10,减200三个异步过程(放延时函数中模拟),输出结果。
每个过程都有10%的概率失败。
提供一个有效区间,比如10 ~ 10000,一旦超出这个范围,抛出异常,当然也可以出错退出。
采用回调函数的方式,代码大概是这样子的:
const Min = 10;
const Max = 10000;
function isSucces() {
const temp = Math.random() * 10;
if ((Math.random() * 100) < 90) {
return true;
} else {
return false;
}
}
function add2(intputValue, successCallback, failCallback) {
setTimeout(() => {
if (!isSucces()) {
failCallback(`add2 计算过程出现异常`);
return;
}
const outputVaulue = intputValue + 2;
if (outputVaulue < Min) {
failCallback(`add2 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
failCallback(`add2 结果${outputVaulue}超出上限${Max}`);
return;
}
successCallback(outputVaulue);
}, 500);
}
function multiply10(intputValue, successCallback, failCallback) {
setTimeout(() => {
if (!isSucces()) {
failCallback(`multiply10 计算过程出现异常`);
return;
}
const outputVaulue = intputValue * 10;
if (outputVaulue < Min) {
failCallback(`multiply10 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
failCallback(`multiply10 结果${outputVaulue}超出上限${Max}`);
return;
}
successCallback(outputVaulue);
}, 1500);
}
function minus200(intputValue, successCallback, failCallback) {
setTimeout(() => {
if (!isSucces()) {
failCallback(`minus200 计算过程出现异常`);
return;
}
const outputVaulue = intputValue - 200;
if (outputVaulue < Min) {
failCallback(`minus200 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
failCallback(`minus200 结果${outputVaulue}超出上限${Max}`);
return;
}
successCallback(outputVaulue);
}, 1000);
}
function cascade(inputValue) {
console.log(`input value: ${inputValue}`);
add2(inputValue, (add2Vaulue) => {
multiply10(add2Vaulue, (multiply10Value) => {
minus200(multiply10Value, (minus200Value) => {
console.log(`output value: ${minus200Value}`);
}, (error) => {
console.log(error);
});
}, (error) => {
console.log(error);
});
}, (error) => {
console.log(error);
});
}
cascade(22);
// input value: 22
// output value: 40 ; (22 + 2) * 10 - 200 = 40
如果采用Promise包装,代码大概是这样子的:
const Min = 10;
const Max = 10000;
function isSucces() {
const temp = Math.random() * 10;
if ((Math.random() * 100) < 90) {
return true;
} else {
return false;
}
}
function add2(intputValue) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!isSucces()) {
reject(`add2 计算过程出现异常`);
return;
}
const outputVaulue = intputValue + 2;
if (outputVaulue < Min) {
reject(`add2 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
reject(`add2 结果${outputVaulue}超出上限${Max}`);
return;
}
resolve(outputVaulue);
}, 500);
});
}
function multiply10(intputValue) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!isSucces()) {
reject(`multiply10 计算过程出现异常`);
return;
}
const outputVaulue = intputValue * 10;
if (outputVaulue < Min) {
reject(`multiply10 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
reject(`multiply10 结果${outputVaulue}超出上限${Max}`);
return;
}
resolve(outputVaulue);
}, 1500);
});
}
function minus200(intputValue) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!isSucces()) {
reject(`minus200 计算过程出现异常`);
return;
}
const outputVaulue = intputValue - 200;
if (outputVaulue < Min) {
reject(`minus200 结果${outputVaulue}超出下限${Min}`);
return;
}
if (outputVaulue > Max) {
reject(`minus200 结果${outputVaulue}超出上限${Max}`);
return;
}
resolve(outputVaulue);
}, 1000);
});
}
function cascade(inputValue) {
console.log(`input value: ${inputValue}`);
add2(inputValue).then(multiply10).then(minus200).then((value) => {
console.log(`output value: ${value}`);
}).catch(error => {
console.log(error);
});
}
cascade(22);
// input value: 22
// output value: 40 ; (22 + 2) * 10 - 200 = 40
通过比较上面两份代码,大部分是相同的。
对于每个异步过程,都用一个Promise对象包装一下。大致的结构一般是这样的,外面再包一层容器函数是为了方便传参数:
function container(param) {
return new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
}
相对于回调函数,原本用
successCallback
的地方用resolve
代替,原本用failCallback
的地方用reject
代替。注意,错误不要用throw
,用reject
代替。差别大的地方是调用,就是上面的
cascade
函数。通过比较,可以看出,Promise
的方式更符合人的认知习惯,看上去更加简洁高效。
在这种多个异步过程级联的场景下,Promise
很有优势。这个就是经常说的,Promise
解决异步调用中的“回调地狱”问题。将
catch
放在最后,集中处理错误,这是一个好习惯,也大大简化了编码。
简单调用的例子
简化上面的需求,只要求加2就可以了。前面大部分的代码都差不多,就是最后的调用有点差别。
回调函数的调用,大致是这样的:
function single(inputValue) {
console.log(`input value: ${inputValue}`);
add2(inputValue, (outputVaulue) => {
console.log(`output value: ${outputVaulue}`);
}, (error) => {
console.log(error);
});
}
single(22);
// input value: 22
// output value: 24; 22 + 2 = 24
Promise
的调用,大致是这样的:
function single(inputValue) {
console.log(`input value: ${inputValue}`);
add2(inputValue).then((value) => {
console.log(`output value: ${value}`);
}).catch(error => {
console.log(error);
});
}
single(22);
// input value: 22
// output value: 24; 22 + 2 = 24
在这种简单调用的场景下,回调函数和
Promise
调用代码的复杂度基本上是差不多的。回调函数的代码更集中。
successCallback
和failCallback
都在调用函数的参数位置。Promise
方式的调用和结果处理是解耦的,是一种分而治之的思想。异步调用过程在Promise
对象创建的时候自动就开始了。
异步过程结束后,成功的结果就交给then
处理,这里做的事情就相当于successCallback
。;失败的结果就交给catch
处理,这里做的事情就相当于failCallback
。Promise的状态在任何时候都是确定的,状态变化是单向的,不可逆的。一开始是
Pending
,异步过程结束时,成功就是Fulfilled
;失败就是Rejected
几个异步过程都成功,再执行下一步的例子
这种场景在平时还是会遇到的。比如,将一张大图分成几张小图下载,小图都下载完后,才能进入下一步的拼接工作。
又比如,用户登录之后,要下载数据,又要进行校验,只有这些过程都成功之后,才能进入下一步。
接上面的需求:就是三个操作一起开始,由于multiply10耗时最长,15秒,那么总的执行时间和这个差不多。
这种场景,有点像比谁跑得慢,最后的结果由最慢的决定。
Promise
提供了一个类方法all
,专门用来应付这种场景。当然,和其他不同的是,这种情况,传递的成功数据是一个数组,而不是一个简单的值。
function all(inputValue) {
console.log(`input value: ${inputValue}`);
console.time("all");
Promise.all([add2(inputValue), multiply10(inputValue), minus200(inputValue)])
.then(values => { // 这里传过来的是结果的数组
values.map(item => {
console.log(`output value: ${item}`);
});
console.timeEnd("all");
})
.catch(error => {
console.log(error);
console.timeEnd("all");
});
}
all(222);
// input value: 222
// output value: 224; 222 + 2 = 224
// output value: 2220; 222 * 10 = 2220
// output value: 22; 222 - 200 = 22
// all: 1502.470947265625ms ; 15秒左右,由最慢的multiply10决定
- 有了all这个函数帮助,跟简单场景基本上差不多。但是如果采用回调函数的方式就比较麻烦了。
几个异步过程有一个成功,就执行下一步的例子
这种场景在现实中也会遇到,比如连接多个主站,只要有一个连上,就登上了,其他的自然就可以丢弃。
这种场景,有点像比谁跑得快,最后的结果由最快的决定。
接上面的需求,执行时间大约5s左右,由最快的add2决定,另外两个耗时长的不会执行。
Promise
提供了一个类方法race
,专门用来应付这种场景。如果用回调函数来实现,还是比较麻烦的。
function race(inputValue) {
console.log(`input value: ${inputValue}`);
console.time("race");
Promise.race([add2(inputValue), multiply10(inputValue), minus200(inputValue)])
.then(value => {
console.log(`output value: ${value}`);
console.timeEnd("race");
})
.catch(error => {
console.log(error);
console.timeEnd("race");
});
}
race(222);
// input value: 222
// output value: 224; 222 + 2 = 224
// race: 504.19677734375ms ; 5秒左右,由最慢的add2决定