工程师们,你们总说 Pormise
好用!但有时候用起来是不是还很懵逼。本文传授给你九条实用的 Promise 使用技巧,帮助你和它建立起良好的关系!
1. 你可以在 .then
回调里返回 Promise
我必须大声喊出来:
是的!你可以
.then
回调里返回 Promise!
而且,返回的 promise 会在接下来的 .then
被自动打开(unwrapped):
.then(r => { // 这是一个 { statusCode: 200 } promise return serverStatusPromise(r); }) .then(resp => { // 200;注意上面的 promise 被自动 unwrap 了 console.log(resp.statusCode); })
2. 每次调用 .then
都会产生一个新的 Promise
如果你熟悉 JavaScript 的链式调用,对这种用法一定不陌生。
调用 .then
和 .catch
时都会创建一个新的 Promise。这个新的 Promise 可以继续使用 .then
或者 .catch
。
var statusProm = fetchServerStatus(); var promA = statusProm.then(r => (r.statusCode === 200 ? "good" : "bad")); var promB = promA.then(r => (r === "good" ? "ALL OK" : "NOTOK")); var promC = statusProm.then(r => fetchThisAnotherThing());
上面出现的 promise 可以用下面这个流程图来表示:
重点注意, promA
、promB
和 promC
虽然相关,但都是不同的 promise 实例。
我喜欢把这种 .then
链当做一个巨大的水暖系统,如果父节点出了故障,热水将无法流入到自节点中。例如,如果 promB
故障了,其他节点不受影响,但是如果 statusProm
出问题了,其他节点就会被影响,也就是被 rejected
。
3. 在任何情况下,Promise resolve/reject
状态都是一致的
这是 Promise 之所以好用的原因。简单理解,就是如果一个 promise 在多个地方使用,当它被 resolve
或者 reject
的时候,都会获得通知。
而且 promise 是无法被修改的,因此它可以随意传递。
function yourFunc() { const yourAwesomeProm = makeMeProm(); // 无论坏叔叔如何消费你的 promise,你的 promise 都可以正常工作 yourEvilUncle(yourAwesomeProm); return yourAwesomeProm.then(r => importantProcessing(r)); } function yourEvilUncle(prom) { // 坏叔叔 return prom.then(r => Promise.reject("destroy!!")); }
Promise 的设计避免了恶意的破坏,如我所说:“没事,可以把 promise 随便扔!”
4. Promise 构造函数不是万金油
我发现有些工程师在任何地方都会使用 Promise 的 constructor,还认为这就是 promise 的使用方式。这是不对的,根本原因就是 constructor API 与原来 callback API 很像,老的习惯很难改。
如果你的代码中遍布 Promise constructor,你的做法就是错的!
如果你想更进一步,摆脱回调函数,你应该尽量减少 Promise 构造函数的使用。
Promise 构造函数正确的使用场景如下:
return new Promise((res, rej) => { fs.readFile("/etc/passwd", function(err, data) { if (err) return rej(err); return res(data); }); });
Promise constructor
只在将回调转成 promise 时使用。
看一个冗余的例子:
// 错误用法 return new Promise((res, rej) => { var fetchPromise = fetchSomeData(.....); fetchPromise .then(data => { res(data); // 错误的方式 }) .catch(err => rej(err)) }) // 正确用法 // 看上去对就是对的 return fetchSomeData(...);
在 Node.js 中,推荐使用 util.promisify。用来将回调 API 转成 promise 式的:
const {promisify} = require('util'); const fs = require('fs'); const readFileAsync = promisify(fs.readFile); readFileAsync('myfile.txt', 'utf-8') .then(r => console.log(r)) .catch(e => console.error(e));
5. 使用 Promise.resolve
JavaScript 提供了 Promise.resolve
API,是产生 Promise 对象的一种快捷方式,这个 promise 对象是被 resolve
的。
var similarProm = new Promise(res => res(5)); // 相当于 var prom = Promise.resolve(5);
这有很多使用场景,我最喜欢的一个是,将一个同步的对象转成一个 promise:
// 将一个同步函数转成异步的 function foo() { return Promise.resolve(5); }
也可以用来在不确定返回值是普通对象还是 promise 时,将返回值封装为 promise 对象:
function goodProm(maybePromise) { return Promise.resolve(maybePromise); } goodProm(5).then(console.log); // 5 // 这个 promise resolve 成 5 var sixPromise = fetchMeNumber(6); goodProm(sixPromise).then(console.log); // 6 // 5,注意,每层 promise 都被自动 unwrap 了 goodProm(Promise.resolve(Promise.resolve(5))).then(console.log);
6. 使用 Promise.reject
与 Promise.resolve
类似,它也是一种快捷写法
var rejProm = new Promise((res, reject) => reject(5)); rejProm.catch(e => console.log(e)) // 5
我最喜欢的 Promise.reject 的用法是,尽早地 reject:
function foo(myVal) { if (!mVal) { return Promise.reject(new Error('myVal is required')) } return new Promise((res, rej) => { // 这些将巨大的 callback 转成 promise }) }
在 .then
中使用 reject:
.then(val => { if (val != 5) { return Promise.reject('Not Good'); } }) .catch(e => console.log(e)) // Not Good
7. 使用 Promise.all
JavaScript 还提供了 Promise.all,但它不是什么快捷方式。
可以如下总结它的算法:
接受一个 promise 的数组 等待所有这些 promise 完成 返回一个新的 Promise,将所有的 resolve 结果放进一个数组里 只要有一个 promise 失败/rejected,这个新的 promise 将会被 rejected
下例展示了所有 promise 都 resolve 的情况:
var prom1 = Promise.resolve(5); var prom2 = fetchServerStatus(); // returns a promise of {statusCode: 200} Proimise.all([prom1, prom2]) .then([val1, val2] => { // notice that it resolves into an Array console.log(val1); // 5 console.log(val2.statusCode); // 200 })
下例展示有一个失败的情况:
var prom1 = Promise.reject(5); var prom2 = fetchServerStatus(); // returns a promise of {statusCode: 200} Proimise.all([prom1, prom2]) .then([val1, val2] => { console.log(val1); console.log(val2.statusCode); }) .catch(e => console.log(e)) // 5, jumps directly to .catch
注意:
Promise.all
是一点不笨,只要有一个 promise 被 reject 了,它就直接 reject,不会等到其他 promise 完成。
8. 不要害怕 reject 或者不要在每一个 .then
后面使用 .catch
我们是不是常常感到有很多隐藏的错误没有被处理?
不用担心像下面这样写:
return fetchSomeData(...);
你可以在任何你想处理的地方解决或者延续 rejection。
处理掉 rejection
这很简单,在 .catch
回调中,无论你返回什么都会变成 resolve,除非你返回一个 Promise.reject
,才会延续 rejection。
.then(() => 5.length) // <-- something wrong happenned here .catch(e => { return 5; // <-- making javascript great again }) .then(r => { console.log(r); // 5 }) .catch(e => { console.error(e); // this function will never be called :) })
reject rejection
reject rejection 的方法就是什么都不做。通常,父函数比起当前函数更擅长处理 rejection。
要记住一个要点,一旦你写了 .catch
就意味着 rejection 已经被处理了,这与同步的 try/catch
类似。
如果你确实想要阻断 rejection(我强烈不推荐这么做):
.then(() => 5.length) // <-- something wrong happenned here .catch(e => { errorLogger(e); // do something impure return Promise.reject(e); // reject it, Yes you can do that! }) .then(r => { console.log(r); // this .then (or any subsequent .then) will never be called as we rejected it above :) }) .catch(e => { console.error(e); //<-- it becomes this catch's problem })
.then(x,y)
和 then(x).catch(x)
.then
接受第二个回调参数来处理错误。虽然与 then(x).catch(x)
看一起类似,但却有所不同,不同点在于可捕获的错误。
下面例子很好地说了这个问题:
.then(function() { return Promise.reject(new Error('something wrong happened')); }).catch(function(e) { console.error(e); // something wrong happened }); .then(function() { return Promise.reject(new Error('something wrong happened')); }, function(e) { // callback handles error coming from the chain above the current `.then` console.error(e); // no error logged });
9. 避免 .then
嵌套
这个原则理解起来很简单,就是避免在 .then
里面继续使用 .then
或者 .catch
。相信我,这绝对是可以避免的。
// 错误用法 request(opts) .catch(err => { if (err.statusCode === 400) { return request(opts) .then(r => r.text()) .catch(err2 => console.error(err2)) } }) // 正确用法 request(opts) .catch(err => { if (err.statusCode === 400) { return request(opts); } return Promise.reject(err); }) .then(r => r.text()) .catch(err => console.erro(err));
就算是如下这种情况,也可以使用 Promise.all
来解决:
.then(myVal => { const promA = foo(myVal); const promB = anotherPromMake(myVal); return promA .then(valA => { return promB.then(valB => hungryFunc(valA, valB)); // very hungry! }) })
可以像下面这样:
.then(myVal => { const promA = foo(myVal); const promB = anotherPromMake(myVal); return Promise.all([prom, anotherProm]) }) .then(([valA, valB]) => { // putting ES6 destructing to good use console.log(valA, valB) // all the resolved values return hungryFunc(valA, valB) })
好了,真心希望本文可以帮到你更好地理解 Promise!
原文标题: Promise 使用技巧九则
原文出处: 前端外刊评论
原文链接: https://qianduan.group/posts/5a9be8980cf6b624d2239cbd