本文大多数内容翻译自该篇文章
Promise可以认为是一种用来解决异步处理的代码规范。常见的异步处理是使用回调函数,回调函数有两种模式,同步的回调和异步的回调。一般回调函数指的是异步的回调。
同步回调
function add(a, b, callback) { callback(a + b) } console.log('before'); add(1, 2, result => console.log('Result: ' + result); console.log('after');
输出结果为: before Result:3 after
异步回调
function addAsync(a, b, callback) { setTimeout( () => callback(a + b), 1000); } console.log('before'); addAsync(1, 2, result => console.log('Result: ' + result)); console.log('after');
输出结果: before after Result: 3
然而回调函数有个著名的坑就是“callback hell”,比如:
doSomething1(function(value1) { doSomething2(function(value2) { doSomething3(function(value3) { console.log("done! The values are: " + [value1, value2, value3].join(',')); }) }) })
为了等value1, value2, value3数据都准备好,必须要一层一层嵌套回调函数。如果一直嵌套下去,就形成了callback hell,不利于代码的阅读。
如果改用Promise的写法,只要写成如下方式就行。
doSomething1().then(function() { return value1; }).then(function(tempValue1) { return [tempValue1, value2].join(','); }).then(function(tempValue2) { console.log("done! ", [tempValue2, value3].join(',')); });
可以注意到,Promise实际上是把回调函数从 doSomething
函数中提取到了后面的 then
方法里面,从而防止多重嵌套的问题。
一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。它要么解析成功,要么失败抛出异常。它允许你以一种同步的方式编写异步代码。
Promise的实现是根据Promises/A+规范实现的。
2.Promise对象和状态
对于Promise的基本使用和入门,可以参考promise-book。这里对Promise的使用做了比较详细的介绍。
2.1 resolve & reject
Promise构造函数用来构造一个Promise对象,其中入参匿名函数中 resolve
和 reject
这两个也都是函数。如果 resolve
执行了,则触发promise.then中成功的回调函数;如果 reject
执行了,则触发promise.then中拒绝的回调函数。
var promise = new Promise(function(resolve, reject) { // IF 如果符合预期条件,调用resolve resolve('success'); // ELSE 如果不符合预期条件,调用reject reject('failure') })
2.2 Fulfilled & Rejected
Promise对象一开始的值是Pending准备状态。
执行了 resolve()
后,该Promise对象的状态值变为onFulfilled状态。
执行了 reject()
后,该Promise对象的状态值变为onRejected状态。
Promise对象的状态值一旦确定(onFulfilled或onRejected),就不会再改变。即不会从onFulfilled转为onRejected,或者从onRejected转为onFulfilled。
2.3 快捷方法
获取一个onFulfilled状态的Promise对象:
Promise.resolve(1); // 等价于 new Promise((resolve) => resolve(1));
获取一个onRejected状态的Promise对象:
Promise.reject(new Error("BOOM")) // 等价于 new Promise((resolve, reject) => reject(new Error("BOOM")));
更多快捷方法请参考Promise API。
3.异常捕获:then和catch
Promise的异常捕获有两种方式:
-
then
匿名函数中的reject
方法 -
catch
方法
3.1 then中的reject方法捕获异常
这种方法只能捕获前一个Promise对象中的异常,即调用 then
函数的Promise对象中出现的异常。
var promise = Promise.resolve(); promise.then(function() { throw new Error("BOOM!") }).then(function (success) { console.log(success); }, function (error) { // 捕捉的是第一个then返回的Promise对象的错误 console.log(error); });
但该种方法无法捕捉当前Promise对象的异常,如:
var promise = Promise.resolve(); promise.then(function() { return 'success'; }).then(function (success) { console.log(success); throw new Error("Another BOOM!"); }, function (error) { console.log(error); // 无法捕捉当前then中抛出的异常 });
3.2 catch捕获异常
上述栗子若改写成如下形式,最后追加一个catch函数,则可以正常捕捉到异常。
var promise = Promise.resolve(); promise.then(function() { return 'success'; }).then(function (success) { console.log(success); throw new Error("Another BOOM!"); }).catch(function (error) { console.log(error); // 可以正常捕捉到异常 });
catch
方法可以捕获到 then
中抛出的错误,也能捕获前面Promise抛出的错误。 因此建议都通过 catch
方法捕捉异常。
var promise = Promise.reject("BOOM!"); promise.then(function() { return 'success'; }).then(function (success) { console.log(success); throw new Error("Another BOOM!"); }).catch(function (error) { console.log(error); // BOOM! });
值得注意的是: catch
方法其实等价于 then(null, reject)
,上面可以写成:
promise.then(function() { return 'success'; }).then(function (success) { console.log(success); throw new Error("Another BOOM!"); }).then(null, function(error) { console.log(error); })
总结来说就是:
-
使用
promise.then(onFulfilled, onRejected)
的话,在onFulfilled
中发生异常的话,在onRejected
中是捕获不到这个异常的。 -
在
promise.then(onFulfilled).catch(onRejected)
的情况下then
中产生的异常能在.catch
中捕获 -
.then
和.catch
在本质上是没有区别的需要分场合使用。
4.动手逐步实现Promise
了解一个东西最好的方式就是尝试自己实现它,尽管可能很多地方不完整,但对理解内在的运行原理是很有帮助的。
这里主要引用了 JavaScript Promises ... In Wicked Detail 这篇文章的实现,以下内容主要是对该篇文章的翻译。
4.1 初步实现
首先实现一个简单的Promise对象类型。只包含最基本的 then
方法和 resolve
方法, reject
方法暂时不考虑。
function Promise(fn) { // 设置回调函数 var callback = null; // 设置then方法 this.then = function (cb) { callback = cb; }; // 定义resolve方法 function resolve(value) { // 这里强制resolve的执行在下一个Event Loop中执行 // 即在调用了then方法后设置完callback函数,不然callback为null setTimeout(function () { callback(value); }, 1); } // 运行new Promise时传入的函数,入参是resolve // 按照之前讲述的,传入的匿名函数有两个方法,resolve和reject fn(resolve); } function doSomething() { return new Promise(function (resolve) { var value = 42; resolve(value); }); } // 调用自己的Promise doSomething().then(function (value) { console.log("got a value", value); });
好了,这是一个很粗略版的Promise。这个实现连Promise需要的三种状态都还没实现。这个版本主要直观展示了Promise的核心方法: then
和 resolve
。
该版本如果 then
异步调用的话,还是会导致Promise中的callback为null。
var promise = doSomething(); setTimeout(function() { promise.then(function(value) { console.log("got a value", value); })}, 1);
后续通过加入状态来维护Promise,就可以解决这种问题。
4.2 Promise添加状态
通过添加一个字段 state
用来维护Promise的状态,当执行了 resolve
函数后,修改 state
为 resolved
,初始 state
是 pendding
。
function Promise(fn) { var state = 'pending'; // 维护Promise实例的状态 var value; var deferred; // 在状态还处于pending时用于保存回调函数的引用 function resolve(newValue) { value = newValue; state = 'resolved'; if (deferred) { // deferred 有值表明回调已经设置了,调用handle方法处理回调函数 handle(deferred); } } // handle方法通过判断state选择如何执行回调函数 function handle(onResolved) { // 如果还处于pending状态,则先保存then传入的回调函数 if (state === 'pending') { deferred = onResolved; return; } onResolved(value); } this.then = function (onResolved) { // 对then传入的回调函数,调用handle去执行回调函数 handle(onResolved); }; fn(resolve); } function doSomething() { return new Promise(function (resolve) { var value = 42; resolve(value); }); } doSomething().then(function (value) { console.log("got a value", value); });
加入了状态后,可以通过判断状态来解决调用先后顺序的问题:
-
在
resolve()
执行前调用then()
。表明这时还没有value处理好,这时的状态就是pending
,此时先保留then()
传入的回调函数,等调用resolve()
处理好value值后再执行回调函数,此时回调函数保存在deferred
中。 -
在
resolve()
执行后调用then()
。表明这时value已经通过resolve()
处理完成了。当调用then()
时就可以通过调用传入的回调函数处理value值。
该版本的Promise我们可以随意先调用 resolve()
或 pending()
,两者的顺序对程序的执行不会造成影响了。
4.3 Promise添加调用链
Promise是可以链式调用的,每次调用 then()
后都返回一个新的Promise实例,因此要修改之前实现的 then()
方法。
function Promise(fn) { var state = 'pending'; var value; var deferred = null; function resolve(newValue) { value = newValue; state = 'resolved'; if (deferred) { handle(deferred); } } // 此时传入的参数是一个对象 function handle(handler) { if (state === 'pending') { deferred = handler; return; } // 如果then没有传入回调函数 // 则直接执行resolve解析value值 if (!handler.onResolved) { handler.resolve(value); return; } // 获取前一个then回调函数中的解析值 var ret = handler.onResolved(value); handler.resolve(ret); } // 返回一个新的Promise实例 // 该实例匿名函数中执行handle方法,该方法传入一个对象 // 包含了传入的回调函数和resolve方法的引用 this.then = function (onResolved) { return new Promise(function (resolve) { handle({ onResolved: onResolved, // 引用上一个Promise实例then传入的回调 resolve: resolve }); }); }; fn(resolve); } function doSomething() { return new Promise(function (resolve) { var value = 42; resolve(value); }); } // 第一个then的返回值作为第二个then匿名函数的入参 doSomething().then(function (firstResult) { console.log("first result", firstResult); return 88; }).then(function (secondResult) { console.log("second result", secondResult); });
then
中是否传入回调函数也是可选的,如:
doSomething().then().then(function(result) { console.log('got a result', result); });
在 handle()
方法的实现中,如果没有回调函数,直接解析已有的value值,该值是上一个Promise实例中调用 resolve(value)
中传入的。
if(!handler.onResolved) { handler.resolve(value); return; }
如果回调函数中返回的是一个Promise对象而不是一个具体数值怎么办?此时我们需要对返回的Promise调用 then()
方法。
doSomething().then(function(result) { // doSomethingElse returns a promise return doSomethingElse(result); }).then(function(anotherPromise) { anotherPromise.then(function(finalResult) { console.log("the final result is", finalResult); }); });
每次这样写很麻烦,我们可以在我们的Promise中的 resole()
方法内处理掉这种情况。
function resolve(newValue) { // 通过判断是否有then方法判断其是否是Promise对象 if (newValue && typeof newValue.then === 'function') { // 递归执行resolve方法直至解析出值出来, // 通过handler.onResolved(value)解析出值,这里handler.onResolve就是resolve方法 newValue.then(resolve); return; } state = 'resolved'; value = newValue; if (deferred) { handle(deferred); } }
4.4 Promise添加reject处理
直至目前为止,已经有了一个比较像样的Promise了,现在添加一开始忽略的 reject()
方法,使得我们可以这样使用Promise。
doSomething().then(function(value) { console.log('Success!', value); }, function(error) { console.log('Uh oh', error); });
实现也很简单, reject()
方法与 resolve()
方法类似。
function Promise(fn) { var state = 'pending'; var value; var deferred = null; function resolve(newValue) { if (newValue && typeof newValue.then === 'function') { newValue.then(resolve, reject); return; } state = 'resolved'; value = newValue; if (deferred) { handle(deferred); } } // 添加的reject方法,这里将Promise实例的状态设为rejected function reject(reason) { state = 'rejected'; value = reason; if (deferred) { handle(deferred); } } function handle(handler) { if (state === 'pending') { deferred = handler; return; } var handlerCallback; // 添加state对于rejected状态的判断 if (state === 'resolved') { handlerCallback = handler.onResolved; } else { handlerCallback = handler.onRejected; } if (!handlerCallback) { if (state === 'resolved') { handler.resolve(value); } else { handler.reject(value); } return; } var ret = handlerCallback(value); handler.resolve(ret); } this.then = function (onResolved, onRejected) { return new Promise(function (resolve, reject) { handle({ onResolved: onResolved, onRejected: onRejected, resolve: resolve, reject: reject }); }); }; fn(resolve, reject); } function doSomething() { return new Promise(function (resolve, reject) { var reason = "uh oh, something bad happened"; reject(reason); }); } // 调用栗子 doSomething().then(function (firstResult) { // wont get in here console.log("first result:", firstResult); }, function (error) { console.log("got an error:", error); });
目前我们的异常处理机制只能处理自己抛出的异常信息,对于其他的一些异常信息是无法正常捕获的,如在 resolve()
方法中抛出的异常。我们对此做如下修改:
function resolve(newValue) { try { // ... as before } catch(e) { reject(e); } }
这里通过添加 try catch
手动捕获可能出现的异常,并在 catch
中调用 reject()
方法进行处理。同样对于回调函数,执行时也可能出现异常,也需要做同样的处理。
function handle(deferred) { // ... as before var ret; try { ret = handlerCallback(value); } catch(e) { handler.reject(e); return; } handler.resolve(ret); }
上述完整的演示代码请查看原文作者提供的fiddle。
4.4 Promise保证异步处理
到目前为止,我们的Promise已经实现了基本比较完善的功能了。这里还有一点需要注意的是,Promise规范提出不管是 resolve()
还是 reject()
,执行都必须保持异步处理。要实现这一点很简单,只需做如下修改即可:
function handle(handler) { if(state === 'pending') { deferred = handler; return; } setTimeout(function() { // ... as before }, 1); }
问题是为什么要这么处理?这主要是为了保证代码执行流程的一致性和可靠性。考虑如下栗子:
var promise = doAnOperation(); invokeSomething(); promise.then(wrapItAllUp); invokeSomethingElse();
通过代码的意图应该是希望 invokeSomething()
和 invokeSomethingElse()
都执行完后,再执行回调函数 wrapItAllUp()
。如果Promise的 resolve()
处理不是异步的话,则执行顺序变为 invokeSomething()
-> wrapItAllUp()
-> invokeSomethingElse()
,跟预想的产生不一致。
为了保证这种执行顺序的一致性,Promise规范要求 resolve
必须是异步处理的。
到这一步,我们的Promise基本像模像样了。当然离真正的Promise还有一段差距,比如缺乏了常用的便捷方法如 all()
, race()
等。不过本例子实现的方法本来就是从理解Promise原理出发的,相信通过该例子对Promise原理会有比较深入的了解。
参考
- JavaScript Promises ... In Wicked Detail 。
- Promises/A+ 。
- promise-book
- A quick guide to JavaScript Promises
- MDN web docs
- Node.js Design Patterns
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。