Promise
1.什么是Promise
1.1. 什么是Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件。
Promise最初被提出是在 E语言中, 它是基于并列/并行处理设计的一种编程语言。
现在JavaScript也拥有了这种特性,基于JavaScript的异步处理,我想大多数都会想到利用回调函数。
//使用了回调函数的异步处理 getAsync("fileA.txt", function(error, result){// 传给回调函数的参数为(error对象, 执行结果)组合 if(error){// 取得失败时的处理 throw error; } // 取得成功时的处理 });
像上面这样基于回调函数的异步处理如果统一参数使用规则的话,写法也会很明了。 但是,这也仅是编码规约而已,即使采用不同的写法也不会出错。
而Promise则是把类似的异步处理对象和处理规则进行规范化, 并按照采用统一的接口来编写,而采取规定方法之外的写法都会出错。
//下面是使用了Promise进行异步处理的一个例子 var promise = getAsyncPromise("fileA.txt"); // 返回promise对象 promise.then(function(result){ // 获取文件内容成功时的处理 }).catch(function(error){ // 获取文件内容失败时的处理 });
简单来说,promise的功能就是可以将复杂的异步处理轻松地进行模式化, 这也可以说得上是使用promise的理由之一。
1.2. Promise简介
在ES6 Promise标准中定义的API中大致有下面三种类型
Constructor
Promise类似于 XMLHttpRequest
,从构造函数 Promise
来创建一个新建新promise
对象作为接口。
要想创建一个promise对象、可以使用new
来调用Promise
的构造器来进行实例化。
var promise = new Promise(function(resolve, reject) { // 异步处理 // 处理结束后、调用resolve 或 reject });
Instance Method
对通过new生成的promise对象为了设置其值在 resolve(成功) / reject(失败)时调用的回调函数 可以使用promise.then()
实例方法。
promise.then(onFulfilled, onRejected)
resolve(成功)时
onFulfilled
会被调用
reject(失败)时
onRejected
会被调用
onFulfilled
、onRejected
两个都为可选参数。
promise.then 成功和失败是都可以使用,如只想对异常进行处理则可以这样写
promise.then(onRejected)//只对异常进行处理
Static Method
Promise全局对象还拥有一些静态方法。
包括 Promise.all()
还有 Promise.resolve()
等在内,主要都是一些对Promise进行操作的辅助方法。
1.2.1. Promise workflow
promise-workflow.js
function asyncFunction() { //new Promise构造器之后,会返回一个promise对象 return new Promise(function (resolve, reject) { setTimeout(function () { resolve('Async Hello world'); }, 16); }); } //为promise对象用设置 .then 调用返回值时的回调函数 asyncFunction().then(function (value) { console.log(value); // => 'Async Hello world' }).catch(function (error) { console.log(error); });
asyncFunction
这个函数会返回promise对象, 对于这个promise对象,我们调用它的 then
方法来设置resolve后的回调函数, catch
方法来设置发生错误时的回调函数。
当然,像promise.then(onFulfilled, onRejected)
的方法声明一样, 如果不使用catch
方法只使用 then
方法的话,如下所示的代码也能完成相同的工作。
asyncFunction().then(function (value) { console.log(value); }, function (error) { console.log(error); });
1.2.2. Promise的状态
用 new Promise
实例化的promise对象有以下三个状态。
"has-resolution" - Fulfilled
resolve(成功)时。此时会调用 onFulfilled
"has-rejection" - Rejected
reject(失败)时。此时会调用 onRejected
"unresolved" - Pending
既不是resolve也不是reject的状态。也就是promise对象刚被创建后的初始化状态等
1.3. 编写Promise代码
1.3.1. 创建promise对象
创建promise对象的流程如下所示。
-
new Promise(fn)
返回一个promise对象 -
在
fn
中指定异步等处理-
处理结果正常的话,调用
resolve(处理结果值)
-
处理结果错误的话,调用
reject(Error对象)
-
按这个流程我们来实际编写下promise代码吧。
我们的任务是用Promise来通过异步处理方式来获取XMLHttpRequest(XHR)的数据。
创建XHR的promise对象
首先,创建一个用Promise把XHR处理包装起来的名为 getURL
的函数。
xhr-promise.js
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } // 运行示例 var URL = "http://httpbin.org/get"; getURL(URL).then(function onFulfilled(value){ console.log(value); }).catch(function onRejected(error){ console.error(error); });
getURL
只有在通过XHR取得结果状态为200时才会调用 resolve
- 也就是只有数据取得成功时,而其他情况(取得失败)时则会调用 reject
方法。
1.3.2. 编写promise对象处理方法
让我们在实际中使用一下刚才创建的返回promise对象的函数
getURL("http://example.com/"); // => 返回promise对象
promise对象拥有几个实例方法, 我们使用这些实例方法来为promise对象创建依赖于promise的具体状态、并且只会被执行一次的回调函数。
为promise对象添加处理方法主要有以下两种
-
promise对象被 resolve 时的处理(onFulfilled)
-
promise对象被 reject 时的处理(onRejected)
到此为止我们已经学习了Promise的基本写法。 其他很多处理都是由此基本语法延伸的,也使用了Promise提供的一些静态方法来实现。
2. Chapter.2 - 实战Promise
2.1. Promise.resolve
一般情况下我们都会使用 new Promise()
来创建promise对象,但是除此之外我们也可以使用其他方法。
在这里,我们将会学习如何使用 Promise.resolve
和 Promise.reject
这两个方法。
2.1.1. new Promise的快捷方式
静态方法Promise.resolve(value)
可以认为是 new Promise()
方法的快捷方式。
比如 Promise.resolve(42);
可以认为是以下代码的语法糖。
new Promise(function(resolve){ resolve(42); });
在这段代码中的 resolve(42);
会让这个promise对象立即进入确定(即resolved)状态,并将 42
传递给后面then里所指定的 onFulfilled
函数。
方法 Promise.resolve(value);
的返回值也是一个promise对象,所以我们可以像下面那样接着对其返回值进行 .then
调用。
Promise.resolve(42).then(function(value){ console.log(value); })
Promise.resolve
方法另一个作用就是将 thenable 对象转换为promise对象。
简单总结一下 Promise.resolve
方法的话,可以认为它的作用就是将传递给它的参数填充(Fulfilled)到promise对象后并返回这个promise对象。
此外,Promise的很多处理内部也是使用了 Promise.resolve
算法将值转换为promise对象后再进行处理的。
2.2. Promise.reject
Promise.reject(error)
是和 Promise.resolve(value)
类似的静态方法,是 new Promise()
方法的快捷方式。
比如 Promise.reject(new Error("出错了"))
就是下面代码的语法糖形式。
new Promise(function(resolve,reject){ reject(new Error("出错了")); });
这段代码的功能是调用该promise对象通过then指定的 onRejected
函数,并将错误(Error)对象传递给这个 onRejected
函数。
Promise.reject(new Error("BOOM!")).catch(function(error){ console.error(error); });
它和Promise.resolve(value)
的不同之处在于promise内调用的函数是reject而不是resolve,这在编写测试代码或者进行debug时,说不定会用得上。
2.3. 专栏: Promise只能进行异步操作?
在使用Promise.resolve(value)
等方法的时候,如果promise对象立刻就能进入resolve状态的话,那么你是不是觉得 .then
里面指定的方法就是同步调用的呢?
实际上, .then
中指定的方法调用是异步进行的。
var promise = new Promise(function (resolve){ console.log("inner promise"); // 1 resolve(42); }); promise.then(function(value){ console.log(value); // 3 }); console.log("outer promise"); // 2
2.3.1. 同步调用和异步调用同时存在导致的混乱
function onReady(fn) { var readyState = document.readyState; if (readyState === 'interactive' || readyState === 'complete') { setTimeout(fn, 0);//异步(同步直接调 fn()) } else { window.addEventListener('DOMContentLoaded', fn); } } onReady(function () { console.log('DOM fully loaded and parsed'); }); console.log('==Starting==');
前面我们看到的 promise.then
也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式 。
如果将上面的 onReady
函数用Promise重写的话,代码如下面所示。
onready-as-promise.js
function onReadyPromise() { return new Promise(function (resolve, reject) { var readyState = document.readyState; if (readyState === 'interactive' || readyState === 'complete') { resolve(); } else { window.addEventListener('DOMContentLoaded', resolve); } }); } onReadyPromise().then(function () { console.log('DOM fully loaded and parsed'); }); console.log('==Starting==');
由于Promise保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout
来自己实现异步调用。
2.4. Promise#then
promise可以写成方法链的形式
aPromise.then(function taskA(value){ // task A }).then(function taskB(vaue){ // task B }).catch(function onRejected(error){ console.log(error); });
2.4.1. promise chain
promise-then-catch-flow.js
function taskA() { console.log("Task A"); } function taskB() { console.log("Task B"); } function onRejected(error) { console.log("Catch Error: A or B", error); } function finalTask() { console.log("Final Task"); } var promise = Promise.resolve(); promise .then(taskA) .then(taskB) .catch(onRejected) .then(finalTask);
上面代码中的promise chain的执行流程,如果用一张图来描述一下的话,像下面的图那样。
Task A产生异常的例子
Task A 处理中发生异常的话,会按照TaskA → onRejected → FinalTask 这个流程来进行处理。
将上面流程写成代码的话如下所示。
promise-then-taska-throw.js
function taskA() { console.log("Task A"); throw new Error("throw Error @ Task A") } function taskB() { console.log("Task B");// 不会被调用 } function onRejected(error) { console.log(error);// => "throw Error @ Task A" } function finalTask() { console.log("Final Task"); } var promise = Promise.resolve(); promise .then(taskA) .then(taskB) .catch(onRejected) .then(finalTask);
执行这段代码我们会发现 Task B 是不会被调用的。
2.4.2. promise chain 中如何传递参数
前面例子中的Task都是相互独立的,只是被简单调用而已。
这时候如果 Task A 想给 Task B 传递一个参数该怎么办呢?
答案非常简单,那就是在 Task A 中 return
的返回值,会在 Task B 执行时传给它。
看下面的例子
promise-then-passing-value.js
function doubleUp(value) { return value * 2; } function increment(value) { return value + 1; } function output(value) { console.log(value);// => (1 + 1) * 2 } var promise = Promise.resolve(1); promise .then(increment) .then(doubleUp) .then(output) .catch(function(error){ // promise chain中出现异常的时候会被调用 console.error(error); });
这段代码的入口函数是 Promise.resolve(1);
,整体的promise chain执行流程如下所示。
-
Promise.resolve(1);
传递 1 给increment
函数 -
函数
increment
对接收的参数进行 +1 操作并返回(通过return
) -
这时参数变为2,并再次传给
doubleUp
函数 -
最后在函数
output
中打印结果
每个方法中 return
的值不仅只局限于字符串或者数值类型,也可以是对象或者promise对象等复杂类型。
return的值会由 Promise.resolve(return的返回值);
进行相应的包装处理,因此不管回调函数中会返回一个什么样的值,最终 then
的结果都是返回一个新创建的promise对象。
也就是说, Promise#then
不仅仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,创建并返回一个promise对象。
2.5. Promise#catch
实际上 Promise#catch 只是 promise.then(undefined, onRejected);
方法的一个别名而已。 也就是说,这个方法用来注册当promise对象状态变为Rejected时的回调函数
2.5.1. IE8的问题
上面的这张图,是下面这段代码在使用 polyfill 的情况下在个浏览器上执行的结果。
Promise#catch的运行结果
var promise = Promise.reject(new Error("message")); promise.catch(function (error) { console.error(error); });
解决Promise#catch标识符冲突问题(能在IE8及以下版本的浏览器中运行(当然还需要polyfill))
var promise = Promise.reject(new Error("message")); promise["catch"](function (error) { console.error(error); });
或者我们不单纯的使用 catch
,而是使用 then
也是可以避免这个问题的。
使用Promise#then代替Promise#catch
var promise = Promise.reject(new Error('message')); promise.then(undefined,function(error){ console.log(error); })
2.6. 专栏: 每次调用then都会返回一个新创建的promise对象
从代码上乍一看, aPromise.then(...).catch(...)
像是针对最初的 aPromise
对象进行了一连串的方法链调用。
然而实际上不管是 then
还是 catch
方法调用,都返回了一个新的promise对象。
下面我们就来看看如何确认这两个方法返回的到底是不是新的promise对象。
var aPromise = new Promise(function (resolve) { resolve(100); }); var thenPromise = aPromise.then(function (value) { console.log(value); }); var catchPromise = thenPromise.catch(function (error) { console.error(error); }); console.log(aPromise !== thenPromise); // => true console.log(thenPromise !== catchPromise);// => true
===
是严格相等比较运算符,我们可以看出这三个对象都是互不相同的,这也就证明了 then
和 catch
都返回了和调用者不同的promise对象。
// 1: 对同一个promise对象同时调用 `then` 方法 var aPromise = new Promise(function (resolve) { resolve(100); }); aPromise.then(function (value) { return value * 2; }); aPromise.then(function (value) { return value * 2; }); aPromise.then(function (value) { console.log("1: " + value); // => 100 }) // vs // 2: 对 `then` 进行 promise chain 方式进行调用 var bPromise = new Promise(function (resolve) { resolve(100); }); bPromise.then(function (value) { return value * 2; }).then(function (value) { return value * 2; }).then(function (value) { console.log("2: " + value); // => 100 * 2 * 2 });
第1种写法中并没有使用promise的方法链方式,这在Promise中是应该极力避免的写法。这种写法中的 then
调用几乎是在同时开始执行的,而且传给每个 then
方法的 value
值都是 100
。
第2中写法则采用了方法链的方式将多个 then
方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then
方法的 value
的值都是前一个promise对象通过 return
返回的值。
2.7. Promise和数组
2.7.1. 使用Promise#then同时处理多个异步请求
需要事先说明的是 Promise.all
比较适合这种应用场景的需求,因此我们故意采用了大量 .then
的晦涩的写法。
使用了.then
的话,也并不是说能和回调风格完全一致,大概重写后代码如下所示。
multiple-xhr.js
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } }; function main() { function recordValue(results, value) { results.push(value); return results; } // [] 用来保存初始化的值 var pushValue = recordValue.bind(null, []); return request.comment().then(pushValue).then(request.people).then(pushValue); } // 运行的例子 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
将上述代码和回调函数风格相比,我们可以得到如下结论。
-
可以直接使用
JSON.parse
函数 -
函数
main()
返回promise对象 -
错误处理的地方直接对返回的promise对象进行处理
向前面我们说的那样,main的 then
部分有点晦涩难懂。
为了应对这种需要对多个异步调用进行统一处理的场景,Promise准备了 Promise.all
和 Promise.race
这两个静态方法。
2.8. Promise.all
Promise.all
接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then
方法。
之前例子中的 getURL
返回了一个promise对象,它封装了XHR通信的实现。 向 Promise.all
传递一个由封装了XHR通信的promise对象数组的话,则只有在全部的XHR通信完成之后(变为FulFilled或Rejected状态)之后,才会调用 .then
方法。
promise-all-xhr.js
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } }; function main() { return Promise.all([request.comment(), request.people()]); } // 运行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.log(error); });
这个例子的执行方法和 前面的例子 一样。 不过Promise.all
在以下几点和之前的例子有所不同。
-
main中的处理流程显得非常清晰
-
Promise.all 接收 promise对象组成的数组作为参数
在上面的代码中,request.comment()
和 request.people()
会同时开始执行,而且每个promise的结果(resolve或reject时传递的参数值),和传递给 Promise.all
的promise数组的顺序是一致的。
如果像下面那样使用一个计时器来计算一下程序执行时间的话,那么就可以非常清楚的知道传递给 Promise.all
的promise数组是同时开始执行的。
promise-all-timer.js
// `delay`毫秒后执行resolve function timerPromisefy(delay) { return new Promise(function (resolve) { setTimeout(function () { resolve(delay); }, delay); }); } var startDate = Date.now(); // 所有promise变为resolve后程序退出 Promise.all([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ]).then(function (values) { console.log(Date.now() - startDate + 'ms'); // 約128ms console.log(values); // [1,32,64,128] });
timerPromisefy
会每隔一定时间(通过参数指定)之后,返回一个promise对象,状态为FulFilled,其状态值为传给 timerPromisefy
的参数。
而传给 Promise.all
的则是由上述promise组成的数组。
var promises = [ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ];
这时候,每隔1, 32, 64, 128 ms都会有一个promise发生 resolve
行为。
也就是说,这个promise对象数组中所有promise都变为resolve状态的话,至少需要128ms。实际我们计算一下Promise.all
的执行时间的话,它确实是消耗了128ms的时间。
从上述结果可以看出,传递给 Promise.all
的promise并不是一个个的顺序执行的,而是同时开始、并行执行的。
2.9. Promise.race
接着我们来看看和 Promise.all
类似的对多个promise对象进行处理的 Promise.race
方法。
它的使用方法和Promise.all一样,接收一个promise对象数组为参数。
Promise.all
在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理, 与之相对的是
Promise.race
只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。
像Promise.all时的例子一样,我们来看一个带计时器的 Promise.race
的使用例子。
promise-race-timer.js
// `delay`毫秒后执行resolve function timerPromisefy(delay) { return new Promise(function (resolve) { setTimeout(function () { resolve(delay); }, delay); }); } // 任何一个promise变为resolve或reject 的话程序就停止运行 Promise.race([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ]).then(function (value) { console.log(value); // => 1 });
上面的代码创建了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后, .then
注册的回调函数就会被调用,这时候确定状态的promise对象会调用 resolve(1)
因此传递给 value
的值也是1,控制台上会打印出1
来。
下面我们再来看看在第一个promise对象变为确定(FulFilled)状态后,它之后的promise对象是否还在继续运行。
var winnerPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is winner'); resolve('this is winner'); }, 4); }); var loserPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is loser'); resolve('this is loser'); }, 1000); }); // 第一个promise变为resolve后程序停止 Promise.race([winnerPromise, loserPromise]).then(function (value) { console.log(value); // => 'this is winner' });
我们在前面代码的基础上增加了 console.log
用来输出调试信息。
执行上面代码的话,我们会看到 winnter和loser promise对象的 setTimeout
方法都会执行完毕, console.log
也会分别输出它们的信息。
也就是说, Promise.race
在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。
2.10. then or catch?
此外我们也会学习一下,在 .then
里同时指定处理对错误进行处理的函数相比,和使用 catch
又有什么异同。
2.10.1. 不能进行错误处理的onRejected
我们看看下面的这段代码。
function throwError(value) { // 抛出异常 throw new Error(value); } // <1> onRejected不会被调用 function badMain(onRejected) { return Promise.resolve(42).then(throwError, onRejected); } // <2> 有异常发生时onRejected会被调用 function goodMain(onRejected) { return Promise.resolve(42).then(throwError).catch(onRejected); } // 运行示例 badMain(function(){ console.log("BAD"); }); goodMain(function(){ console.log("GOOD"); });
在上面的代码中, badMain
是一个不太好的实现方式(但也不是说它有多坏), goodMain
则是一个能非常好的进行错误处理的版本。
为什么说 badMain
不好呢?,因为虽然我们在 .then
的第二个参数中指定了用来错误处理的函数,但实际上它却不能捕获第一个参数 onFulfilled
指定的函数(本例为 throwError
)里面出现的错误。
也就是说,这时候即使 throwError
抛出了异常,onRejected
指定的函数也不会被调用(即不会输出"BAD"字样)。
与此相对的是, goodMain
的代码则遵循了 throwError
→onRejected
的调用流程。 这时候 throwError
中出现异常的话,在会被方法链中的下一个方法,即 .catch
所捕获,进行相应的错误处理。
.then
方法中的onRejected参数所指定的回调函数,实际上针对的是其promise对象或者之前的promise对象,而不是针对 .then
方法里面指定的第一个参数,即onFulfilled所指向的对象,这也是 then
和 catch
表现不同的原因。
|
这种情况下 then
是针对 Promise.resolve(42)
的处理,在onFulfilled
中发生异常,在同一个 then
方法中指定的 onRejected
也不能捕获该异常。
在这个 then
中发生的异常,只有在该方法链后面出现的 catch
方法才能捕获。
当然,由于 .catch
方法是 .then
的别名,我们使用 .then
也能完成同样的工作。只不过使用 .catch
的话意图更明确,更容易理解。
Promise.resolve(42).then(throwError).then(null, onRejected);
2.10.2. 总结
这里我们又学习到了如下一些内容。
我们需要注意如果代码类似 badMain
那样的话,就可能出现程序不会按预期运行的情况,从而不能正确的进行错误处理。