本人JS萌新一枚,最近在编写NodeJS服务器逻辑的时候遇到了大量异步并发、异步顺序逻辑的问题,于是终于学会了Promise的用法,因此记录下来与大家分享。
1 Promise的基础用法:
let prom = new Promise(function (resolve, reject) {
resolve('resolve');
});
prom.then(function (data) {
console.log(data);
},function () {
console.log('error!');
});
输出
resolve
Promise内的函数有两个参数,resolve和reject,但是这两个函数绝对不是让用户定义的回调函数。而是Promise对象自动传递的方法。当调用resolve(param)函数时,当前Promise对象的状态由pending转变为resolved,同时将value传递给Promise.then()方法定义的resolve函数。同样,调用reject(param)会将状态由pending转变为rejected,并将param传递给then()方法定义的的reject函数。
也可以理解为Promise.then()方法中的两个函数是实参,new Promise()中的resolve和reject是形参,这种“先调用函数,再定义函数”的逻辑也就是promise(承诺)的类名的由来。
状态只能改变一次,之后的resolve()和reject()都不会被执行。
2 Promise.then()的链式调用
2.1 基础用法
let prom = new Promise(function (resolve, reject) {
console.log('Origin Promise');
resolve();
});
prom.then(function() { console.log('first then') })
.then(function() { console.log('second then') })
.then(function() { console.log('third then') });
输出结果如下:
Origin Promise
first then
second then
third then
2.2 链式传参
首先,Promise.then()方法之所以可以链式调用,是因为then()方法隐式返回了一个Promise对象,相当于:
Promise.then(function () {
// do something
return new Promise(function (resolve, reject) { resolve() }); // auto return
})
同时我们知道,Promise对象通过调用resolve函数来向下一步传递参数。然而默认的then()方法返回的Promise对象不会传递任何参数。也就是说,默认的链式调用方法只能用于顺序执行程序而不能传参。
想要让每一步之间互相关联(传参),就必须显式地返回我们自定义的Promise对象。
let prom = new Promise(function (resolve, reject) {
let data = 'Important data!';
resolve(data);
});
prom.then(function(incomeData) {
return new Promise(function (resolve) { resolve(incomeData) });
})
.then(function(incomeData) {
return new Promise(function (resolve) { console.log(incomeData) });
});
// Important data!
每次都要返回那么长一串代码是不是很麻烦呢?所以我们可以自己写一个修饰函数。
// 一个Promise修饰函数
let myThen = function (fun) {
return function (data) {
return new Promise(function (resolve) { resolve(fun(data)) });
}
};
// 使用示例
myThen(() => 'Important data!')()
.then(myThen(data => data))
.then(data => console.log(data));
// Important data!
使用上面的修饰后的函数,就可以专注于编写业务逻辑,而不用关心参数传递了。通过普通的return就可以传递参数。
当然,我的解决方案也不是那么美观(多了一层函数嵌套),各位在了解Promise原理后也可以书写自己喜欢的封装函数。
另外说一个偏方:可以在Promise之外定义一个变量,用于存储Promise各个步骤之间传递的参数,但是这个参数不能在Promise之外的地方调用(无法确认其内容),且占用的内存在Promise逻辑全部执行完毕之前不能释放,可能存在内存溢出问题。
2.3 生成“子线程”
一般来说,一个Promise对象用于一个异步顺序逻辑。但是实际上,一个Promise对象可以接多个then()方法,这时,每个then()方法都会生成一个新的Promise对象,各个对象之间相互独立,且可以继承相同的父代参数,但是互相之间不能传参。当然Node.js是单线程的,所以这里只是模拟了多个子线程而已。
2.4 易错点
这里记录几个我在使用过程中出现的易错点:
(1)new Promise与Promise.then()方法不同,Promise.then()方法可以不传入任何参数而正常执行(相当于执行了一个空的步骤而进入下一步),而new Promise必须传入一个函数作为参数,且该函数最好有一个resolve参数并且被调用过。
new Promise没有传入函数:报错
new Promise传入函数没有resolve或resolve未被调用(且未报错或reject):Promise始终处于pending状态且不执行下一步
(2)有时候为了让程序更容易理解,可能会在一段链式调用逻辑结束以后,重新调用对象名来强调一下逻辑依然在该链式调用之后。例如:
let prom = new Promise(function (resolve, reject) {
let data = 'Important data!';
resolve(data);
});
// 注意要将返回的对象传回原变量
prom = prom.then(function(incomeData) {
return new Promise(function (resolve) { resolve(incomeData) });
})
prom.then(function(incomeData) {
return new Promise(function (resolve) { console.log(incomeData) });
});
// Important data!
注意要将then()方法返回的Promise对象传回原变量,这样才能正常地执行下去,否则将分成两个独立的异步顺序逻辑(类似于一个线程生成两个子线程)。
3 Promise.catch()
一般来说,我们不建议使用reject方法,而是使用catch方法。
调用reject表示“程序出现了我意料之中的错误,这个错误可以被正常地处理”并进入reject分支。
调用catch表示“程序出现了错误(不论是否提前考虑到了),这个错误将被处理”,随后进入catch()方法,程序结束。
3.1 Promise.reject()用法
来看看reject的用法:
let prom = new Promise(function (resolve, reject) {
let data = 'Failed!';
reject(data); // 进入reject
});
prom.then(function() {}, function(e) {
console.log(e, 'reject');
return Promise.reject(); // 继续reject
})
.then(function () {}, function () {
console.log('next reject!');
})
.catch(function(e) {console.log(e, 'catch')});
// Failed! reject
// next reject!
看到这里我们就明白为什么我们不推荐使用reject了,因为reject也是可以链式调用的!其实reject跟resolve处于平等的地位,相当于if与else的区别,即便我们不使用resolve,一直使用reject进行链式调用也不会有任何问题。更过分的是:
let prom = new Promise(function (resolve, reject) {
throw 'error!'; // 抛出错误
});
prom.then(function() {}, function(e) {
console.log(e, 'reject'); // reject捕获了error
})
.catch(function(e) {console.log(e, 'catch')}); // 没有进入catch分支
// error! reject
reject也“包办”了catch的功能!
这还没完,再看一段代码:
let prom = new Promise(function (resolve, reject) {
throw 'error!';
});
prom.then(function() {}, function(e) {
console.log(e, 'reject');
return new Promise(function (resolve, reject) { resolve() }); // 调用resolve
})
.then(function () {
console.log('next resolve!'); // 由reject进入resolve
}, function () {
console.log('next reject!');
})
.catch(function(e) {console.log(e, 'catch')});
// error! reject
// next resolve!
没错,reject分支也可以进入下一步的resolve分支!
所以说,reject和resolve只是名字不同的两个参数而已,没必要经常使用reject,真的没多大区别……
3.2 Promise.catch()用法
Promise.catch()的用法就没那么多坑了:
let prom = new Promise(function (resolve, reject) {
throw 'first error!'; // 第一个报错
resolve();
});
prom.then(function() {
throw 'second error!';
})
.then(function () {
throw 'third error!';
})
.catch(function (e) { console.log(e) });
// first error!
let prom = new Promise(function (resolve, reject) {
// throw 'first error!';
resolve();
});
prom.then(function() {
throw 'second error!'; // 第二个报错
})
.then(function () {
throw 'third error!';
})
.catch(function (e) { console.log(e) })
// second error!
可以看到catch可以捕捉到链式调用中任意一步抛出的错误,并且后续指令都不执行。
另外:
let prom = new Promise(function (resolve, reject) {
// throw 'first error!';
reject('first reject!'); // 调用reject
});
prom.then(function() {
throw 'second error!';
})
.then(function () {
throw 'third error!';
})
.catch(function (e) { console.log(e) })
// first reject!
reject方法传递的参数也可以使用catch()捕获,reject的存在意义又少了一层……
当然这也许就是resolve与reject最大的区别了……
4 Promise.all()
4.1 基础用法
Promise.all()接收一个Promise对象数组,并监视该数组中的Pormise对象,当所有Promise都进入resolved之后执行then():
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 2 complete!');
resolve('p2');
}, 2000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
})
输出结果如下:
Promise 1 complete!
Promise 2 complete!
Promise 3 complete!
All complete!
data: [ 'p1', 'p2', 'p3' ]
Promise.all()会监视数组中所有Promise对象并将其结果作为一个数组传递给then()方法。
Promise.all()接收的数组也可以包含非Promise元素,当数组元素不是Promise对象时,会自动生成一个resolved状态的Promise对象,并且将元素作为参数传递下去。
4.2 有reject的情况
现在我们让其中一个Promise变成rejected状态:
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 2 complete!');
reject('p2'); // 这里reject
}, 2000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
}, function (rejectData) {
console.log('Reject: ', rejectData); // 进入rejected分支
})
结果如下:
Promise 1 complete!
Promise 2 complete!
Reject: p2
Promise 3 complete!
可以看到一旦有一个Promise变为rejected,Promise.all()就会直接进入then(),且会将reject的传参传递给then中定义的rejected分支函数(未定义rejected分支函数的话会报错),其他Promise会继续执行,但是无论是已经完成的还是未完成的Promise都不再监听,且其结果也没有接收。
4.3 有报错的情况
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 2 complete!');
throw 'p2 error!'; // 添加报错
}, 2000);
})
.catch(function (e) { console.log('local log: ', e) }); // 尝试本地处理
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
}).catch(function (e) { // 尝试捕获
console.log('error: ', e);
})
结果如下:
Promise 1 complete!
Promise 2 complete!
D:\test\test.js:39
throw 'p2 error!';
^
p2 error!
结果我们可以发现,不论是在本地捕获还是在all()之后捕获,都是无效的!Error直接穿过了Promise对象,导致程序报错退出。
而且我们知道,try...catch...方法对于异步逻辑是无效的。因此,想要捕捉Promise.all()内部的报错,只能在每个Promise的异步操作执行的时候捕捉。
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
// 本地捕捉error
try{
console.log('Promise 2 complete!');
throw 'p2 error!';
}
catch (e){
console.log('local error: ', e);
}
}, 2000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
try{
Promise.all([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
})
}
catch (e) {
console.log('Error: ', e);
}
输出如下:
Promise 1 complete!
Promise 2 complete!
local error: p2 error!
Promise 3 complete!
程序没有退出,但是Promise.all()还是终止了,且没有触发catch()方法。
从这里我们可以看出,Promise还是存在诸多问题的。
5 Promise.race()
5.1 基础用法
Promise.race()方法也接收一个Promise对象数组,当元素不是Promise对象时会自动生成resolved状态的Promise对象并将元素作为参数传递。
Promise.race()当任何一个Promise对象完成时进入resolved状态,其他Promise会继续执行但是不再监控也不获取结果:
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 2000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 2 complete!');
resolve('p2');
}, 1000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
Promise.race([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
});
其结果如下:
Promise 2 complete!
All complete!
data: p2
Promise 1 complete!
Promise 3 complete!
5.2 有reject的情况
当Promise对象中存在reject时:
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 1 complete!');
resolve('p1');
}, 2000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 2 complete!');
reject('p2'); // reject
}, 1000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('Promise 3 complete!');
resolve('p3');
}, 3000);
});
Promise.race([p1, p2, p3]).then(function (data) {
console.log('All complete!');
console.log('data: ', data);
}, function (e) {
console.log('Reject: ', e);
});
打印结果如下:
Promise 2 complete!
Reject: p2
Promise 1 complete!
Promise 3 complete!
由于Promise对象的状态只会转变一次,所以第一个Promise进入resolved状态后,Promise.race()就不再监听了,之后如果有reject也不会产生效果。