关于Promise的9个提示
正如同事所说的那样,promise 在工作中表现优异。
这篇文章会给你一些如何改善与 promise 之间关系的建议。
1. 你可以在 .then 里面 return 一个 promise
让我来说明这最重要的一点
是的!你可以在 .then 里面 return 一个 promise
而且,return 的这个 promise 将在下一个 .then 中自动解析。
.then(r => {
return serverstatuspromise(r); // 返回 { statuscode: 200 } 的 promise
})
.then(resp => {
console.log(resp.statuscode); // 200; 注意自动解析的 promise
})
2. 每次执行 .then 的时候都会自动创建一个新的 promise
如果熟悉 javascript 的链式风格,那么你应该会感到很熟悉。但是对于一个初学者来说,可能就不会了。
在 promise 中不论你使用 .then 或者 .catch 都会创建一个新的 promise。这个 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 的 resolved/rejected 状态是唯一的
我认为这个是让 promise 好好运行的最重要的事情之一。简单来说,如果在你的应用中 promise 在很多不同的模块之间共享,那么当 promise 返回 resolved/rejected 状态时,所有的调用者都会收到通知。
这也意味着没有人可以改变你的 promise,所以可以放心的把它传递出去。
function yourfunc() {
const yourawesomeprom = makemeprom();
youreviluncle(yourawesomeprom); // 无论 promise 受到了怎样的影响,它最终都会成功执行
return yourawesomeprom.then(r => importantprocessing(r));
}
function youreviluncle(prom) {
return prom.then(r => promise.reject("destroy!!")); // 可能遭受的影响
}
通过上面的例子可以看出,promise 的设计使得自身很难被改变。正如我上面所说的:"保持冷静,并将 promise 传递下去"。
4. promise 构造函数不是解决方案
我看到很多开发者喜欢用构造函数的风格,他们认为这就是 promise 的方式。但这却是一个谎言,实际的原因是构造函数 api 和之前回调函数的 api 相似,而且这样的习惯很难改变。
如果你发现自己正在到处使用 promise 构造函数,那你的做法是错的!
要真正的向前迈进一步并且摆脱回调,你需要小心谨慎并且最小程度地使用 promise 构造函数。
让我们看一下使用 promise 构造函数 的具体情况:
return new promise((res, rej) => {
fs.readfile("/etc/passwd", function(err, data) {
if (err) return rej(err);
return res(data);
});
});
promise 构造函数 应该只在你想要把回调转换成 promise 时使用。
一旦你掌握了这种创建 promise 的优雅方式,它将会变的非常有吸引力。
让我们看一下冗余的 promise 构造函数。
错误的
return new promise((res, rej) => {
var fetchpromise = fetchsomedata(.....);
fetchpromise
.then(data => {
res(data); // 错误!!!
})
.catch(err => rej(err))
})
正确的
return fetchsomedata(...); // 正确的!
用 promise 构造函数 封装 promise 是多余的,并且违背了 promise 本身的目的。
高级技巧
如果你是一个 nodejs 开发者,我建议你可以看一看 util.promisify。这个方法可以帮助你把 node 风格的回调转换为 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 方法,像下面的例子这样简洁:
var similarprom = new promise(res => res(5));
// ^^ 等价于
var prom = promise.resolve(5);
它有多种使用情况,我最喜欢的一种是可以把普通的(异步的)js 对象转化成 promise。
// 将同步函数转换为异步函数
function foo() {
return promise.resolve(5);
}
当不确定它是一个 promise 还是一个普通的值的时候,你也可以做一个安全的封装。
function goodprom(maybepromise) {
return promise.resolve(maybepromise);
}
goodprom(5).then(console.log); // 5
var sixpromise = fetchmenumber(6);
goodprom(sixpromise).then(console.log); // 6
goodprom(promise.resolve(promise.resolve(5))).then(console.log); // 5, 注意,它会自动解析所有的 promise!
6.使用 promise.reject
javascript 也提供了 promise.reject 方法。像下面的例子这样简洁:
var rejprom = new promise((res, reject) => reject(5));
rejprom.catch(e => console.log(e)) // 5
我最喜欢的用法是提前使用 promise.reject 来拒绝。
function foo(myval) {
if (!mval) {
return promise.reject(new error('myval is required'))
}
return new promise((res, rej) => {
// 从你的大回调到 promise 的转换!
})
}
简单来说,使用 promise.reject 可以拒绝任何你想要拒绝的 promise。
在下面的例子中,我在 .then 里面使用:
.then(val => {
if (val != 5) {
return promise.reject('not good');
}
})
.catch(e => console.log(e)) // 这样是不好的
注意:你可以像 promise.resolve 一样在 promise.reject 中传递任何值。你经常在失败的 promise 中发现 error 的原因是因为它主要就是用来抛出一个异步错误的。
7. 使用 promise.all
javascript 提供了 promise.all 方法。像 ... 这样的简洁,好吧,我想不出来例子了。
在伪算法中,promise.all 可以被概括为:
接收一个 promise 数组
然后同时运行他们
然后等到他们全部运行完成
然后 return 一个新的 promise 数组
他们其中有一个失败或者 reject,都可以被捕获。
下面的例子展示了所有的 promise 完成的情况:
var prom1 = promise.resolve(5);
var prom2 = fetchserverstatus(); // 返回 {statuscode: 200} 的 promise
proimise.all([prom1, prom2])
.then([val1, val2] => { // 注意,这里被解析成一个数组
console.log(val1); // 5
console.log(val2.statuscode); // 200
})
下面的例子展示了当他们其中一个失败的情况:
var prom1 = promise.reject(5);
var prom2 = fetchserverstatus(); // 返回 {statuscode: 200} 的 promise
proimise.all([prom1, prom2])
.then([val1, val2] => {
console.log(val1);
console.log(val2.statuscode);
})
.catch(e => console.log(e)) // 5, 直接跳转到 .catch
注意:promise.all 是很聪明的!如果其中一个 promise 失败了,它不会等到所有的 promise 完成,而是立即中止!
8. 不要害怕 reject,也不要在每个 .then 后面加冗余的 .catch
我们是不是会经常担心错误会在它们之间的某处被吞噬?
为了克服这个恐惧,这里有一个简单的小提示:
让 reject 来处理上游函数的问题。
在理想的情况下,reject 方法应该是应用的根源,所有的 reject 都会向下传递。
不要害怕像下面这样写
return fetchsomedata(...);
现在如果你想要处理函数中 reject 的情况,请决定是解决问题还是继续 reject。
解决 reject
解决 reject 是很简单的,在 .catch 不论你返回什么内容,都将被假定为已解决的。然而,如果你在 .catch 中返回 promise.reject,那么这个 promise 将会是失败的。
.then(() => 5.length) // <-- 这里会报错
.catch(e => {
return 5; // <-- 重新使方法正常运行
})
.then(r => {
console.log(r); // 5
})
.catch(e => {
console.error(e); // 这个方法永远不会被调用 :)
})
拒绝一个 reject
拒绝一个 reject 是简单的。不需要做任何事情。 就像我刚刚说的,让它成为其他函数的问题。通常情况下,父函数有比当前函数处理 reject 更好的方法。
需要记住的重要的一点是,一旦你写了 catch 方法,就意味着你正在处理这个错误。这个和同步 try/catch的工作方式相似。
如果你确实想要拦截一个 reject:(我强烈建议不要这样做!)
.then(() => 5.length) // <-- 这里会报错
.catch(e => {
errorlogger(e); // 做一些错误处理
return promise.reject(e); // 拒绝它,是的,你可以这么做!
})
.then(r => {
console.log(r); // 这个 .then (或者任何后面的 .then) 将永远不会被调用,因为我们在上面使用了 reject :)
})
.catch(e => {
console.error(e); //<-- 它变成了这个 catch 方法的问题
})
.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) { // 这个回调处理来自当前 `.then` 方法之前的错误
console.error(e); // 没有错误被打印出来
});
当你想要处理的是来自上游 promise 而不是刚刚在 .then 里面加上去的错误的时候, .then(x,y) 变的很方便。
提示: 99.9% 的情况使用简单的 then(x).catch(x) 更好。
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);
}
})
.then(r => r.text())
.catch(err => console.erro(err));
有些时候我们在 .then 里面需要很多变量,那就别无选择了,只能再创建一个 .then 方法链。
.then(myval => {
const proma = foo(myval);
const promb = anotherprommake(myval);
return proma
.then(vala => {
return promb.then(valb => hungryfunc(vala, valb)); // 很丑陋!
})
})
我推荐使用 es6 的解构方法混合着 promise.all 方法就可以解决这个问题。
.then(myval => {
const proma = foo(myval);
const promb = anotherprommake(myval);
return promise.all([prom, anotherprom])
})
.then(([vala, valb]) => { // 很好的使用 es6 解构
console.log(vala, valb) // 所有解析后的值
return hungryfunc(vala, valb)
})
注意:如果你的 node//老板/意识允许,还可以使用 async/await 方法来解决这个问题。