这不是一篇介绍Promise的文章,若是你还不太了解Promise,能够先看下我以前的 关于Promise以及你可能不知道的6件事,以为写得还能够的但愿能动动小手点个赞,谢谢啦(*^▽^*)
不行!說的是一輩子!差一年、一個月、一天、一個時辰...都不算一輩子!-- 程蝶衣
承诺(Promise)始终应该是承诺(Promise),即便落空,也应该是一个失败(Rejected)的承诺(Promise);node
Promise 对象渐渐成为了现代 JavaScript 程序异步接口的标准返回。Promise 相对于 Callback
,拥有两个先天的优点:git
If you have an APIwhich takes a callback,
and sometimes that callback is called immediately,
and other times that callback is called at some point in the future,
then you will render any code using this API impossible to reason about, andcause the release of Zalgo.
咱们重点来看第二点,一样也是Callback的一个重大缺点,就是结果太不可控了,除非咱们百分之百肯定这个接口是异步的,不然有可能出现上文所说的状况,这个接口一下子是异步的(第一次网络请求),一下子是同步的(直接返回本地Cache),并且更糟糕的是,若是这个做者仇视社会的话,没准还会调用好几回回调,而这些都是你无法控制的(┑( ̄Д ̄)┍摊手)。而这些 Callback 的缺点一样是 Promise 的卖点,但你觉得用了 Promise 就大功告成了嘛: No!github
// 一个简单的除法程序 function divide(numerator, denominator) { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { throw new TypeError('Must be number!'); } if (denominator === 0) { throw new Error("Cannot divide by 0!"); } return new Promise((resolve, reject) => { resolve(numerator / denominator); }); }
好了,一个还算严谨的除法程序(原谅我用Promise实现),作了类型校验,还作了被除数非 0 的校验,给你 3 秒钟说一下这程序有什么问题,3...2...等不及了,这个程序最大的问题在于,虽然用 Promise不像回调那样会很明显的把异步和同步返回混淆,但一不当心,咱们把校验的逻辑写成了同步的。这时候若是一味天真的少年用了咱们这个“强大”的 Promise 函数。npm
// 用着挺好 divide(3, 1) .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > Get: 3 // 测试下错误状况 divide(3, 0) .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > Error: Cannot divide by 0! at divide ... ... 咦,怎么抛错了,不是都写了 catch 了吗,少年心灰意冷地看了一下源码,“MD,智障,我来改一下吧”,咱们的实现被深深鄙视了一番。 // 一个简单的除法程序改进版 function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { reject(new TypeError('Must be number!')); } if (denominator === 0) { reject(new Error("Cannot divide by 0!")); } resolve(numerator / denominator); }); }
Tips 编程
当咱们本身着手设计一个返回 Promise 对象的函数时,请尽可能都采用当即返回 new Promise 的形式。segmentfault
function promiseFactory() { return new Promise((resolve, reject) => { ... ... }); }
固然,若是咱们的 Promise 工厂函数依赖了另外一个 Promise 对象的结果的时候,也能够直接 return 那个 Promise 对象。promise
function promiseFactory3() { return promiseFactory1() .then(promiseFactory2); }
不少时候,因为咱们的疏忽大意,一些松散的逻辑或者意料以外的输入都会让咱们理想中的 Promise 返回化为泡影。但若是你把全部逻辑都写在 Promise 构造器或 Promise 对象的 then/catch 函数中的话,即便一个意外的输入致使内部抛了错,也能(绝大部分状况下)返回一个 Rejected 的 Promise,而不是一个未捕获的错误。网络
因此,即便用了 Promise,也可能致使 release Zalgo 的发生,因此请你在下次写完一个 Promise 返回的函数的时候,再仔细瞅瞅,它必定会返回一个 Promise 吗?(说好的一生呢,混蛋( ̄ε(# ̄));app
她習慣向左走,他習慣向右走,他們始終未曾相遇。-- 幾米
固然,咱们是在讨论使用 Promise 构造器的用法,你在 then 里面都没 reject 呢。咱们在前一章说过,始终在Promise 构造器中书写逻辑的话,即便出现了意外的输入,也能绝大部分状况下返回一个Rejected 的 Promise,好了,本章讨论的就是其余状况,坦诚说,这一点也很多见。异步
仍是以上一个除法程序为例。
// 一个简单的除法程序 throw 版 function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { throw new TypeError('Must be number!'); } if(denominator === 0) { throw new Error("Cannot divide by 0!"); } resolve(numerator / denominator); }); }
效果和以前是如出一辙的,并且 throw 的用法看起来还更常见,但 reject 和 throw 有一个本质的不一样!reject是回调,而throw只是一个同步的语句,若是在另外一个异步的上下文中抛出,在当前上下文中是没法捕获到的。例以下面的代码,咱们用 setTimeout 模拟一个异步的抛错。
// 一个简单的除法程序异步 throw 版 function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { setTimeout(function() { throw new TypeError('Must be number!'); }, 0); } if(denominator === 0) { throw new Error("Cannot divide by 0!"); } resolve(numerator / denominator); }); } divide('asd', 'asd') .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > Get: NaN > TypeError: Must be number! at ...
果真,这个错误没有被 Promise捕捉到,还致使了另一个问题,咱们成功经过了校验,返回了 NaN,这些都不是咱们想要的结果。
固然一般你也不会写这样的代码,但咱们仍是有那么多的 callback-style 的 API 啊。一不注意就可能写成下面那样。
// 检查文件内容 throw 版 function checkFileContent(file, str) { return new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (err, data) => { if (err) { throw err; } if (!~data.indexOf(str)) { throw new Error(`No such content: ${str}`); } resolve(true); }) }); } checkFileContent('test.js', 'Promise') .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > Get: true checkFileContent('test.js', 'xxx') .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > Error: No such content: xxx at ...
很不幸,这个函数除非彻底知足咱们的预期(包含某些内容的文件),其他状况都会抛出一个咱们没法 catch 到的错误,更不幸的是,这样的错误也没法用try/catch捕捉到,你要不当心写了这样的程序,而且只测试了经过的状况,颇有可能忽然的一天,你的程序就崩溃了。那时,你的心里是否是也要崩溃了呢。
固然,这种异步 throw 的做法在某些状况下也是颇有用的,能够防止未知的错误被 Promise 吞掉,形成程序 Debug 的困难。例如 Q 中的 done 函数,就是相似下面的实现。
Promise.prototype.done = function() { return this.catch(function(e) { setTimeout(function() { throw e; }, 0); }); };
Tips
在 Promise 构造器中,除非你明确知道使用 throw 的正确姿式,不然都请使用 reject。
// 检查文件内容 reject 版 function checkFileContent(file, str) { return new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (err, data) => { if (err) { reject(err); } if (!~data.indexOf(str)) { reject(new Error(`No such content: ${str}`)); } resolve(true); }) }); }
另外,在异步回调函数中,除了咱们本身写的throw语句以外,任何其余缘由形成的错误都会致使抛出咱们没法捕捉到的异常。例如JSON解析,因此,在异步回调中请千万注意,不要出现意料以外的错误抛出,全部可能的错误都请用 reject 明确拒绝。
// 检查文件内容 reject 版 + JSON function checkFileContent(file, str) { return new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (err, data) => { if (err) { reject(err); } if (!~data.indexOf(str)) { reject(new Error(`No such content: ${str}`)); } try { JSON.parse(data); } catch (e) { reject(e); } resolve(true); }) }); }
你見,或者不見我,我就在那裡。不悲不喜。 -- 倉央嘉措
以前说过 Promise 的一大优势,就是结果不变性,一旦 Promise 的值肯定为 fulfilled 或者 rejected 后,不管过多久,获取到的 Promise 对象的值都是同样的。
// 一个简单的除法程序改进版 function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { reject(new TypeError('Must be number!')); } console.log('After validating type...'); if (denominator === 0) { reject(new Error("Cannot divide by 0!")); } console.log('After validating non-zero denominator...'); resolve(numerator / denominator); }); }
如上图所示,咱们在原有程序的基础上增长了一些日志来查看 Promise 内部的执行状态。
divide(3, 1) .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > After validating type... > After validating non-zero denominator... > Get: 3 // 结果看起来很不错。再来测试个错误输入。 divide(3, 0) .then(res => console.log(`Get: ${res}`)) .catch(err => console.log(`Failed: ${err}`)) > After validating type... > After validating non-zero denominator... > Failed: Error: Cannot divide by 0! // !!! 怎么回事
忽然感到这世界森森的恶意,不是说Promise肯定后不变嘛,怎么都reject还接着走。咳咳,少年,不要惊慌,咱们说的是Promise肯定后不变,不表明reject以后函数就不执行了啊,大家年轻人啊,仍是 too young too simple,蛤蛤。
在 JavaScript 函数中,只有return/yield/throw会中断函数的执行,其余的都没法阻止其运行到结束的,这也是所谓的 Run-to-completion 特性。
像 resolve/reject 不过只是一个回调而已,而所谓的不变性只是说,当遇到第一个 resolve/reject 后,便根据其结果给此Promise打上了一个tag,而且不能更改,然后面的该干啥继续干,不干本 Promise 的事儿了。
Tips
解决这个问题的方法也很简单,就是在 resolve/reject 以前加上 return 便可,跟咱们日常函数中的用法同样,固然了,由于这自己就是一个普通的函数嘛。
// 一个简单的除法程序改进版 提早 return function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { return reject(new TypeError('Must be number!')); } console.log('After validating type...'); if (denominator === 0) { return reject(new Error("Cannot divide by 0!")); } console.log('After validating non-zero denominator...'); return resolve(numerator / denominator); // 随便你怎么弄 反正不会执行到我! for (var i = 0, j = 10000; i < j; i++) { doSomething(i); } }); }
对于这段代码来讲,执行后续代码的后果是打印出多余的日志,实际状况确定比这复杂得多,好比某个异步调用或者网络请求,甚至是一个CPU密集型的循环操做,我相信全部这些都不是你想要的,因此请你在resolve/reject语句前面加上return,除非你真的想把后续的代码一直运行到结束。
妳相信壹切都永不會改變。然後妳離開了,壹年,兩年,當妳回來時,壹切都變了。-- 天堂電影院
现代 Web 的不少新颖的 API 都已经采用了 Promise 做为返回,例如你们都很熟悉的 Fetch,还有很让人期待的ServiceWorker
等。然而,这并非一篇介绍如何使用某某 API 的说明书,而是谈另一个问题,在 Promise 和 Callback 同时存在的宇宙上,如何写出一个同时坐拥二者的异步 API。
由于在 Node.js 中,全部的原生异步 API 基本都是采用了 Error-first callbacks,甚至能够被简称了 Node-style 了,例以下面很简单的一个读取文件的例子:
fs.readFile('/foo.txt', function(err, data) { if (err) return; console.log(data); });
好了,咱们试着简单包装一下。若是第二个参数传入了函数,就直接调用原生的readFile。不然,返回一个 Promise。
function readFile2(filename, cb) { if (typeof cb === 'function') { return fs.readFile(filename, cb); } return new Promise((resolve, reject) => { fs.readFile(filename, function(err, data) { if (err) return reject(err); resolve(data); }); }); }
好了,咱们成功写了一个既能使用 Promise 又能使用 Callback 的函数,这样,不管使用咱们库的用户想要什么 Style 都能一一知足。固然,实际状况比这复杂得多,还得考虑多个参数等的状况,不然 Q: Interfacing with Node.jsCallbacks 中也不会有一堆与 Node-style 交互的函数了。
上面是对原生 API 封装的状况,此外,愈来愈多经常使用的三方库都支持直接返回一个 Promise 对象,例如 mongoose,这时,若是咱们要包装一个同时支持二者的 API 就变得简单了。咱们能够利用 Promise 的链式特性,直接在 Promise 的结尾添加相关逻辑,而无需在中间步骤中反复调用 callback(null, data) 或者 callback(err, null)(这不只仅是麻烦的问题,还会由于逻辑不严谨致使 callback 调用屡次的问题,你看,这又是 Promise 的优势,下降你犯错的几率)。
// 还记得大明湖畔的除法程序嘛 function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { return reject(new TypeError('Must be number!')); } if (denominator === 0) { return reject(new Error("Cannot divide by 0!")); } return resolve(numerator / denominator); }); }
让咱们尝试添加 Callback 支持。
// 除法二代目,能够支持 Callback 了 function divide2(numerator, denominator, callback) { var promise = divide(numerator, denominator); if (typeof callback === 'function') { promise.then(res => { callback(null, res); }, err => { callback(err, null); }); } else { return promise; } }
So easy, 不但这样,并且咱们能够很容易抽象一个函数,对于那些非可变参数的 Promise 工厂函数添加 Callback 返回。实际上,有不少库都写了这样一个函数,我在 NPM 上搜了一圈,找到了一个下载量特别大的,确定靠谱,promise-nodify,啧啧。
var nodify = require('promise-nodify'); function divide(numerator, denominator) { return new Promise((resolve, reject) => { if (typeof numerator !== 'number' || typeof denominator !== 'number' ) { return reject(new TypeError('Must be number!')); } if (denominator === 0) { return reject(new Error("Cannot divide by 0!")); } return resolve(numerator / denominator); }); } // 拥抱 promise-nodify 的三代目 function divide3(numerator, denominator, callback) { var promise = divide(numerator, denominator); if (typeof callback === 'function') { return nodify(promise, callback); } else { return promise; } }
让咱们测试一下:
divide3(3, 1, (err, data) => { console.log(err, data); }); > null 3 divide3(3, 0, (err, data) => { console.log(err, data); }); > [Error: Cannot divide by 0!] null divide3("3", 1, (err, data) => { console.log(err, data); }); > [TypeError: Must be number!] null
完美经过,今后,Promise 和 Callback 手牵手肩并肩,过上了幸福的二人世界。
Happy Ending.
...
...
...
然而,有那么一天,咱们不当心在用 divide3 的时候,手一抖,写错了个字。
divide3(3, 1, (err, data) => { consale.log(err, data); // 把 console 写错了 }); >
你没有看错,什么都没有,编程中最怕的不是报错,而是不报错,若是在你庞大的代码块中有这么一个地方,默默地出现了异常,又默默地消失,不留痕迹,这样太恐怖了。
这一切都是为何,相信你也猜到了,由于 Promise。
来看看 promise-nodify 的源代码。(让我想到了leftPad 事件)
module.exports = function nodify(promise, callback) { if (typeof callback === "function") { promise.then(function (resp) { callback(null, resp); }, function (err) { callback(err, null); }); } };
那咱们的异常是从在哪儿被吞没的呢?
module.exports = function nodify(promise, callback) { if (typeof callback === "function") { promise.then(function (resp) { callback(null, resp); ==》 这句话抛了异常,然而被这个 promise 吞没了。 }, function (err) { callback(err, null); }); } };
相信你们都明白了缘由,再看看这个模块的下载量,不得不为这些用户担心啊 ╮(╯◇╰)╭。
知道了缘由,让咱们试着改一下,就用前面所说的使用 setTimeout 在 Promise 链的结尾异步抛错。
module.exports = function nodify2(promise, callback) { if (typeof callback === "function") { promise.then(function (resp) { callback(null, resp); }, function (err) { callback(err, null); }).catch(function(err) { setTimeout(function() { throw err; }); }); } }; divide3(3, 1, (err, data) => { consale.log(err, data); }); > throw err; ... ReferenceError: consale is not defined ...
终于成功发现了 consale 的拼写错误,妈妈不再担忧咱们出现 typo 了。
Tips
可以兼容 Promise 和 Callback 确实是件很棒的事情,用第三方代码前请尽可能理解其原理,短小的话彻底能够本身写一个。Promise 虽好,可不要乱用哦,实时牢记它会吞没错误的风险。
另外,上面那种实现也是有问题的,仔细看你就会发现,它会使得错误栈多了一层。更好的方法以下:
// 下面使用了 process.nextTick,除此以外,还能够用 setImmediate。具体区别,不赘述了。 module.exports = function nodify3(promise, callback) { if (typeof callback === "function") { promise.then(function (resp) { process.nextTick(callback.bind(null, null, res)); }, function (err) { process.nextTick(callback.bind(null, err, null)); }); } };
但愿你看完以后可以继续喜好并使用 Promise,若是我遇到过的问题可以帮助你的话,那就更好了,Good Luck!