写在前面:编程
这是一篇总结文章,但也能够理解为是一篇翻译,主体脉络参考自下面这篇文章:json
www.mattgreer.org/articles/pr…数组
若英文阅读无障碍,墙裂推荐该文章的阅读。promise
在平常写代码的过程当中,我很常常会用到 promises 语法。当我自觉得了解 promises 详细用法时,却在一次讨论中被问住了:“你知道 promises 内部的实现过程是怎样的么?” 是的,回想起来,我只是知道该如何使用它,殊不知道其内部真正的实现原理。这篇文章正是我本身的关于 promises 的回顾与总结。若是你看完了整篇文章,但愿你也会更加理解 promises 的实现与原理。浏览器
咱们将会从零开始,逐步实现一个本身的 promises。最终的代码将会和 Promises/A+ 规范类似,而且将会明白 promises 在异步编程中的重要性。固然,本文会假设读者已经拥有了关于 promises 的基础知识。bash
让咱们从最简单的 promises 实现开始吧。当咱们想要将下面的代码闭包
doSomething(function(value) {
console.log('Got a value:' + value);
});
复制代码
转变为异步
doSomething().then(function(value) {
console.log('Got a value:' + value);
});
复制代码
这个时候,咱们须要怎么作呢?很是简单的方式就是,将原来的 doSomething()
函数从原来的写法异步编程
function doSomething(callback) {
var value = 42;
callback(value);
}
复制代码
转变为以下这种 'promise' 写法:函数
function doSomething() {
return {
then: function(callback) {
var value = 42;
callback(value);
}
};
}
复制代码
上面只是一个 callback 写法的一种语法糖包装而已,看起来毫无心义。不过,这是个很是重要的转变,咱们已经开始触达了 promises 的一个核心理念:
Promises 捕获最终值( eventual values ),并将其放入到一个 Object 中。
Ps: 这里有必要解释“最终值”的概念。它是异步函数的返回值,状态是不肯定的,有可能成功,也有可能失败(以下图)。
关于 Promises 与最终值( eventual values ),下文会包含更多的讨论。
上面简单的改写并不足以对 promise 的特性作任何的说明,让咱们来定义一个真正的 promise 函数吧:
function Promise(fn) {
var callback = null;
this.then = function(cb) {
callback = cb;
};
function resolve(value) {
callback(value);
}
fn(resolve);
}
复制代码
代码解析:将then
的写法拆分,同时引入了resolve
函数,方便处理 Promise 的传入对象(函数)。同时,使用callback
做为沟通then
函数与resolve
函数的桥梁。这个代码实现,有一点 Promise 该有的样子了,不是么?
在此基础上,咱们的doSomething()
函数将会写成这种形式:
function doSomething() {
return new Promise(function(resolve) {
var value = 42;
resolve(value);
});
}
复制代码
当咱们尝试执行的时候,会发现执行会报错。这是由于,在上面的代码实现中,resolve()
会比then
更早被调用,此时的callback
仍是null
。为了解决这个问题,咱们使用setTimeout
的方式 hack 一下:
function Promise(fn) {
var callback = null;
this.then = function(cb) {
callback = cb;
};
function resolve(value) {
// 强制此处的 callback 在 event loop 的下一个
// 迭代中调用,这样 then()将会在其以前执行
setTimeout(function() {
callback(value);
}, 1);
}
fn(resolve);
}
复制代码
通过这样的修改以后,咱们的代码将能够成功运行。
咱们设想的实现,是能够在异步状况下也能够正常工做的。可是此时的代码,是很是脆弱的。只要咱们的then()
函数中包含有异步的状况,那么变量callback
将会再次变成null
。既然这个代码这么渣渣,为何还要写下来呢?由于上面的模式很方便咱们待会的拓展,同时,这个简单的写法,也可让大脑对then
、resolve
的工做方式有一个初步的了解。下面咱们考虑在此基础上作必定的改进。
Promises 是拥有状态的,咱们须要先了解 Promises 中都有哪些状态:
一个 promise 在等待最终值的时候,将会是 pending 状态,当获得最终值的时候,将会是 resolved 状态。
当一个 promise 成功获得最终值的时候,它将会一直保持这个值,不会再次 resolve。
(固然,一个 promise 的状态也能够是 rejected,下文会细述)
为了将状态引入到咱们的代码实现中,咱们将原来的代码改写为下面:
function Promise(fn) {
var state = 'pending';
//value 表示经过resolve函数传递的参数
var value;
//deferred 用于保存then()里面的函数参数
var deferred;
function resolve(newValue) {
value = newValue;
state = 'resolved';
if(deferred) {
handle(deferred);
}
}
function handle(onResolved) {
if(state === 'pending') {
deferred = onResolved;
return;
}
onResolved(value);
}
this.then = function(onResolved) {
handle(onResolved);
};
fn(resolve);
}
复制代码
这个代码看起来更加复杂了。不过此时的代码可让调用方任意调用then()
方法,也能够任意使用resolve()
方法了。它也能够同时运行在同步、异步的状况下。
代码解析:代码中使用了state
这个flag。同时,then()
与resolve()
将公共的逻辑提取到了一个新的函数handle()
中:
then()
比resolve()
更早被调用的时候,此时的状态是 pending,对应的 value 值并无准备好。咱们将then()
里面对应的回调参数保存在 deferred 中,方便 promise 在获取到 resolved 的时候调用。resolve()
比then()
更早被调用的时候,此时的状态设置为 resolved,对应的 value 值也已经获得。当then()
被调用的时候,直接调用then()
里面对应的回调参数便可。then()
与resolve()
将公共的逻辑提取到了一个新的函数handle()
中,所以无论上面的两个 case 谁被触发,最终都会执行 handle 函数。若是你仔细看会发现,此时的setTimeout
已经不见了。咱们经过 state 的状态控制,已经获得了正确的执行顺序。固然,下面的文章中,还有会使用到setTimeout
的时候。
经过使用 promise,咱们调用对应方法的顺序将不会受到任何影响。只要符合咱们的需求,在任什么时候刻调用
resolve()
比then()
都不会影响其内部逻辑。
此时,咱们能够尝试屡次调用then
方法,会发现每一次获得的都是相同的 value 值。
var promise = doSomething();
promise.then(function(value) {
console.log('Got a value:', value);
});
promise.then(function(value) {
console.log('Got the same value again:', value);
});
复制代码
在咱们平常针对 promises 的编程中,下面的链式模式是常见的:
getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
复制代码
getSomeData()
返回的是一个 promise,此时能够经过调用then()
方法。但值得注意的是,第一个then()
方法的返回值也必须是一个 promise,这样才可让咱们的链式 promises 一直延续下去。
then()
方法必须永远返回一个 promise。
为了实现这个目的,咱们将代码作进一步的改造:
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;
}
if(!handler.onResolved) {
handler.resolve(value);
return;
}
var ret = handler.onResolved(value);
handler.resolve(ret);
}
this.then = function(onResolved) {
return new Promise(function(resolve) {
handle({
onResolved: onResolved,
resolve: resolve
});
});
};
fn(resolve);
}
复制代码
呼啦~ 如今的代码让人看起来彷佛有点抓狂😩。哈哈哈,你是否会庆幸一开始的时候咱们代码不是那么复杂呢?这里面真正的一个关键点在于:then()
方法永远返回一个新的 promise。
doSomething().then(function(result){
console.log("first result : ", result);
return 88;
}).then(function(secondResult){
console.log("second result : ", secondResult);
return 99;
})
复制代码
让咱们来详细看看第二个 promise 的 resolve 过程。它接收来自第一个 promise 的 value 值。详细的过程发生在 handle()
方法的底部。入参handler
带有两个参数:一个是 onResolved
回调,一个是对resolve()
方法的引用。在这里,每个新的 promise 都会有一个对内部方法resolve()
的拷贝以及对应的运行时闭包。这是链接第一个 promise 与第二个 promise 的桥梁。
在代码中,咱们能够获得第一个 promise 的 value 值:
var ret = handler.onResolved(value);
复制代码
在上面的例子中,handler.onResolved
表示的是:
function(result){
console.log("first result : ", result);
return 88;
}
复制代码
也就是说,handler.onResolved
实际上返回的是第一个 promise 的 then 被调用时候的传入参数(函数)。第一个 handler 的返回值被用于第二个 promise 的 resolve 传入参数。
这就是整个链式 promise 的工做方式。
若是咱们想要将全部的 then 返回的结果,该怎么作呢?咱们可使用一个数组,来存放每一次的返回值:
doSomething().then(function(result) {
var results = [result];
results.push(88);
return results;
}).then(function(results) {
results.push(99);
return results;
}).then(function(results) {
console.log(results.join(', ');
});
// the output is
//
// 42, 88, 99
复制代码
promises 永远 resolve 返回的是一个值。当你想要返回多个值的时候,能够经过建立某些符合结构来实现(如数组、object等)。
then()
中的传入参数(回调函数)是并非必填的。若是为空,在链式 promise 中,将会返回前一个 promise 的返回值。
doSomething().then().then(function(result) {
console.log('got a result', result);
});
// the output is
//
// got a result 42
复制代码
你能够查看handle()
中的实现方式,当前一个 promise 没有 then 的传入参数的时候,它会 resolve 前一个 promise 的value 值:
if(!handler.onResolved) {
handler.resolve(value);
return;
}
复制代码
咱们的链式 promise 实现,依然显得有些简单。这里的 resolve 返回的是一个简单的值。假如想要 resolve 返回的是一个新的 promise 呢?好比下面的方式:
doSomething().then(function(result) {
// doSomethingElse 返回的是一个promise
return doSomethingElse(result);
}).then(function(finalResult) {
console.log("the final result is", finalResult);
});
复制代码
若是是这样的状况,那么咱们上面的代码彷佛没法应对这样的状况。对于紧随其后的那个 promise 而言,它获得的 value 值将会是一个 promise。为了获得预期的值,咱们须要这样作:
doSomething().then(function(result) {
// doSomethingElse 返回的是一个promise
return doSomethingElse(result);
}).then(function(anotherPromise) {
anotherPromise.then(function(finalResult) {
console.log("the final result is", finalResult);
});
});
复制代码
OMG... 这样的实现实在是太糟糕了。难道做为使用者,我还要每一次都须要本身来手动书写这些冗余的代码么?是否能够在 promise 代码内部处理一下这些逻辑呢?实际上,咱们只须要在已有代码中的 resolve()
中增长一点判断便可:
function resolve(newValue) {
if(newValue && typeof newValue.then === 'function') {
newValue.then(resolve);
return;
}
state = 'resolved';
value = newValue;
if(deferred) {
handle(deferred);
}
}
复制代码
上面的代码逻辑中咱们看到,resolve()
中若是遇到的是 promise,将会一直迭代调用resolve()
。直到最后得到的值再也不是一个 promise,才会依照已有的逻辑继续执行。
还有一个值得注意的点:看看代码中是如何断定一个对象是否是具备 promise 属性的?经过断定这个对象是否有then
方法。这种断定方法被称为 "鸭子类型"(咱们并不关心对象是什么类型,究竟是不是鸭子,只关心行为)。
这种宽松的界定方式,可使得具体的不一样 promise 实现彼此之间有一个很好地兼容。
在链式 promise 章节中,咱们的实现已经相对而言是很是完整的。可是咱们并无讨论到 promises 中的错误处理。
在 promise 的决议过程当中,若是发生了错误,那么 promise 将会抛出一个拒绝决议,同时给出对应的理由。对于调用者,怎么知道错误发生了呢?能够经过 then()
方法的第二个传入参数(函数):
doSomething().then(function(value) {
console.log('Success!', value);
}, function(error) {
console.log('Uh oh', error);
});
复制代码
正如上面提到的,一个 promise 会从初始状态 pending 转换为要么是resolved 状态,要么是 rejected 状态。这二者,只能有一个做为最终的状态。对应到
then()
的两个参数,只有一个会被真正执行。
在 promise 内部实现中,一样容许有一个reject()
函数来处理 reject 状态,能够看作是 resolve()
函数的孪生兄弟。此时,doSomething()
函数也将会被改写为支持错误处理的方式:
function doSomething() {
return new Promise(function(resolve, reject) {
var result = somehowGetTheValue();
if(result.error) {
reject(result.error);
} else {
resolve(result.value);
}
});
}
复制代码
对于此,咱们的代码该作如何的对应改造呢?来看代码:
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);
}
}
function reject(reason) {
state = 'rejected';
value = reason;
if(deferred) {
handle(deferred);
}
}
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
var handlerCallback;
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);
}
复制代码
代码解析:不只仅新增了一个reject()
函数,并且handle()
方法内部也增长了对 reject
的逻辑处理:经过对state
的判断,来决定具体执行handler
的 reject
/resolved
。
上面的代码,只对已知的错误进行了处理。当发生某些不可知错误的时候,一样应该引起 rejection。须要在对应的处理函数中增长try...catch
:
首先是在resolve()
方法中:
function resolve(newValue) {
try {
// ... as before
} catch(e) {
reject(e);
}
}
复制代码
一样的,在 handle()
执行具体 callback
的时候,也可能发生未知的错误:
function handle(handler) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
handler.reject(e);
return;
}
handler.resolve(ret);
}
复制代码
有时候,对于 promises 的错误解读,将会致使 promises 吞下错误。这是个常常坑开发者的点。
让咱们来考虑这个例子:
function getSomeJson() {
return new Promise(function(resolve, reject) {
var badJson = "<div>uh oh, this is not JSON at all!</div>";
resolve(badJson);
});
}
getSomeJson().then(function(json) {
var obj = JSON.parse(json);
console.log(obj);
}, function(error) {
console.log('uh oh', error);
});
复制代码
这段代码将会如何进行呢?在then()
中的 resolve 执行的是对 JSON 的解析。它觉得可以执行,结果却抛出了异常,由于传入的 value 值并非 JSON 格式。咱们写了一个 error callback 来捕获这个错误。这样是没有问题,对吧?
不,结果可能并不符合你的指望。此时的 error callback 并不会触发。结果将会是:控制台上没有任何的 log 输出。这个错误就这样被平静地吞掉了。
为何会这样?由于咱们的错误发生在then()
的 resolve 回调内部,源码上看是发生在 handle()
方法内部。这将会致使的是,then()
返回的新的 promise 将会被触发 reject,而不是现有的这个 promise 会触发 reject:
function handle(handler) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
// 到达这里,触发的是handler.reject()
// 这是then()返回的新的promise的reject()
// 若是改为 handler.onRejected(ex),将会触发本promise的reject()
handler.reject(e);
return;
}
handler.resolve(ret);
}
复制代码
若是将上面代码中的catch
部分改写成:handler.onRejected(ex);
将会触发的是本 promise 的reject()
。但这就违背了 promises 的原则:
一个 promise 会从初始状态 pending 转换为要么是 resolved 状态,要么是 rejected 状态。这二者,只能有一个做为最终的状态。对应到
then()
的两个参数,只有一个会被真正执行。
由于已经触发了 resolved 状态,那么久不可能再次触发 rejected 状态。错误是在具体执行 resolved 函数的时候发生的,那么这个 error,将会被下一个 promise 捕获。
咱们能够这样验证:
getSomeJson().then(function(json) {
var obj = JSON.parse(json);
console.log(obj);
}).then(null, function(error) {
console.log("an error occured: ", error);
});
复制代码
这多是 promises 中最坑人的一个点了。固然,只要理解了其中的原因,那么就能够很好地避免。为了更好地体验,咱们有什么解决方法来规避这个坑呢?请看下一节:
大部分的 promise 库都包含有一个 done()
方法。它实现的功能和then()
方法类似,只是很好的规避了刚刚提到的then()
的坑。
done()
方法能够像then()
那样被调用。二者之间主要有两点不一样:
done()
方法返回的不是一个 promisedone()
中的任何错误将不会被 promise 实现捕获(直接抛出)在咱们的例子中,若是使用done()
方法,将会更加保险:
getSomeJson().done(function(json) {
// when this throws, it won't be swallowed
var obj = JSON.parse(json);
console.log(obj);
});
复制代码
从 promise 中的 rejection 恢复是有可能的。若是在一个包含有 rejection 的 promise 中增长更多的then()
方法,那么从这个then()
开始,将会延续链式 promise 的正常处理流程:
aMethodThatRejects().then(function(result) {
// won't get here
}, function(err) {
// since aMethodThatRejects calls reject()
// we end up here in the errback
return "recovered!";
}).then(function(result) {
console.log("after recovery: ", result);
}, function(err) {
// we won't actually get here
// since the rejected promise had an errback
});
// the output is
// after recovery: recovered!
复制代码
在本文的开头,咱们使用了一个 hack 来让咱们的简单代码可以正确容许。还记得么?使用了一个 setTimeout
。当咱们完善了对应的逻辑以后,这个 hack 就没有再使用了。但事实是:Promises/A+ 规范要求 promise 决议必须是一步的。为了实现这个需求,最简单的作法就是再次使用 setTimeout
将咱们的handle()
方法包装一层:
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
setTimeout(function() {
// ... as before
}, 1);
}
复制代码
很是简单的实现。可是,实际上的 promises 库并不倾向于使用setTimeout
。若是对应的库是用于 NodeJS,那么它们倾向于使用 process.nextTick
,若是对应的库是用于浏览器,那么它们倾向于使用setImmediate
。
具体的作法咱们知道了,可是为何规范中会有这样的要求呢?
为了确保一致性与可信赖的执行过程。让咱们考虑这样的状况:
var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
复制代码
上面的代码会被怎样执行呢?基于命名,你可能设想这个执行过程会是这样的:invokeSomething()
-> invokeSomethingElse()
-> wrapItAllUp()
。但实际上,这取决于在咱们当前的实现过程当中,promise 的 resolve 过程是同步的仍是异步的。若是doAnOperation()
的 promise 执行过程是异步的,那么其执行过程将会是设想的流程。若是doAnOperation()
的 promise 执行过程是同步的,它真实的执行过程将会是invokeSomething()
-> wrapItAllUp()
-> invokeSomethingElse()
。这时,可能会致使某些意想不到的后果。
所以,为了确保一致性与可信赖的执行过程。promise 的 resolve 过程被要求是异步的,即便自己可能只是简单的同步过程。这样作,可让全部的使用体验都是一直的,开发者在使用过程当中,也再也不须要担忧各类不一样的状况的兼容。
若是读到了这里,那么能够肯定是真爱了!咱们将 promises 的核心概念都讲了一遍。固然,文章中的代码实现,大部分都是简陋的。可能也会和真正的代码库实现有必定的出入。但但愿不妨碍您对总体 promises 的理解。更多的关于 promises 的实现细节(如:all()
、race
等),能够查看更多的文档与源码实现。
当真正理解了 promises 的工做原理以及它的一些边界状况,我才真正喜欢上它。今后个人项目中关于 promises 的代码也变得更加简洁。关于 promises,还有不少内容值得去探讨,本文只是一个开始。