Promise,相信每一个前端工程师都或多或少地在项目中都是用过,毕竟它早已不是一个新名词。ES6中已经原生对它加以支持,在caniuse中搜索一下 Promise
,发现新版的chrome和firefox也已经支持。但是低版本的浏览器我们可以使用 es6-promise
这个 polyfill
库来加以兼容。
暂且不谈 await
、 async
,在Google或百度或360搜索等搜索引擎、或者在segmentfault等社区中,我们可以搜到一大把介绍 promise
的文章,毕竟它已经出现了很长时间,早已有很多大神分析讲解过。
我也看了一些文章,但是感觉都没有达到想要的效果。所以决定自己开一个小系列文章学习讲解一下promise的原理,以及实现,最后再谈一谈与之联系密切的Deferred对象。
本文是该系列的第一篇文章,主要先让大家对Promise有一个基本的认识。
Promise的出现,原本是为了解决回调地狱的问题。所有人在讲解 Promise
时,都会以一个ajax请求为例,此处我们也用一个简单的ajax的例子来带大家看一下 Promise
是如何使用的。
ajax请求的传统写法:
getData(method, url, successFun, failFun){ var xmlHttp = new XMLHttpRequest(); xmlHttp.open(method, url); xmlHttp.send(); xmlHttp.onload = function () { if (this.status == 200 ) { successFun(this.response); } else { failFun(this.statusText); } }; xmlHttp.onerror = function () { failFun(this.statusText); }; }
改为 promise
后的写法:
getData(method, url){ var promise = new Promise(function(resolve, reject){ var xmlHttp = new XMLHttpRequest(); xmlHttp.open(method, url); xmlHttp.send(); xmlHttp.onload = function () { if (this.status == 200 ) { resolve(this.response); } else { reject(this.statusText); } }; xmlHttp.onerror = function () { reject(this.statusText); }; }) return promise; } getData('get','www.xxx.com').then(successFun, failFun)
很显然,我们把异步中使用回调函数的场景改为了 .then()
、 .catch()
等函数链式调用的方式。基于 promise
我们可以把复杂的异步回调处理方式进行模块化。
下面,我们就来介绍一下 Promise
到底是个什么东西?它是如何做到的?
Promise
的原理分析
其实 promise
原理说起来并不难,它内部有三个状态,分别是 pending
, fulfilled
和 rejected
。
pending
是对象创建后的初始状态,当对象 fulfill
(成功)时变为 fulfilled
,当对象 reject
(失败)时变为 rejected
。且只能从 pengding
变为 fulfilled
或 rejected
,而不能逆向或从 fulfilled
变为 rejected
、从 rejected
变为 fulfilled
。如图所示:
<img src=" http://ohjvilcfv.bkt.clouddn.... width="300px" />
Promise
实例方法介绍
Promise
对象拥有两个实例方法 then()
和 catch()
。
从前面的例子中可以看到,成功和失败的回调函数我们是通过 then()
添加,在 promise
状态改变时分别调用。 promise
构造函数中通常都是异步的,所以 then
方法往往都先于 resolve
和 reject
方法执行。所以 promise
内部需要有一个存储 fulfill
时调用函数的数组和一个存储 reject
时调用函数的数组。
从上面的例子中我们还可以看到 then
方法可以接收两个参数,且通常都是函数(非函数时如何处理下一篇文章中会详细介绍)。第一个参数会添加到 fulfill
时调用的数组中,第二个参数添加到 reject
时调用的数组中。当 promise
状态 fulfill
时,会把 resolve(value)
中的 value
值传给调用的函数中,同理,当 promise
状态 reject
时,会把 reject(reason)
中的 reason
值传给调用的函数。例:
var p = new Promise(function(resolve, reject){ resolve(5) }).then(function(value){ console.log(value) //5 }) var p1 = new Promise(function(resolve, reject){ reject(new Error('错误')) }).then(function(value){ console.log(value) }, function(reason){ console.log(reason) //Error: 错误(…) })
then
方法会返回一个新的 promise
,下面的例子中 p == p1
将返回 false
,说明 p1
是一个全新的对象。
var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) }) p == p1 // false
这也是为什么 then
是可以链式调用的,它是在新的对象上添加成功或失败的回调,这与 jQuery
中的链式调用不同。
那么新对象的状态是基于什么改变的呢?是不是说如果 p
的状态 fulfill
,后面的 then
创建的新对象都会成功;或者说如果 p
的状态 reject
,后面的 then
创建的新对象都会失败?
var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) // 5 }).then(function(value){ console.log('fulfill ' + value) // fulfill undefined }, function(reason){ console.log('reject ' + reason) })
上面的例子会打印出5和"fulfill undefined"说明它的状态变为成功。那如果我们在 p1
的 then
方法中抛出异常呢?
var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) // 5 throw new Error('test') }).then(function(value){ console.log('fulfill ' + value) }, function(reason){ console.log('reject ' + reason) // reject Error: test })
理所当然,新对象肯定会失败。
反过来如果 p
失败了,会是什么样的呢?
var p = new Promise(function(resolve, reject){ reject(5) }) var p1 = p.then(undefined, function(value){ console.log(value) // 5 }).then(function(value){ console.log('fulfill ' + value) // fulfill undefined }, function(reason){ console.log('reject ' + reason) })
说明新对象状态不会受到前一个对象状态的影响。
再来看如下代码:
var p = new Promise(function(resolve, reject){ reject(5) }) var p1 = p.then(function(value){ console.log(value) }) var p2 = p1.then(function(value){ console.log('fulfill ' + value) }, function(reason){ console.log('reject ' + reason) // reject 5 })
我们发现 p1
的状态变为 rejected
,从而触发了 then
方法第二个参数的函数。这似乎与我们之前提到的有差异啊, p1
的状态受到了 p
的状态的影响。
再来看一个例子:
var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(undefined, function(value){ console.log(value) }) var p2 = p1.then(function(value){ console.log('fulfill ' + value) // fulfill 5 }, function(reason){ console.log('reject ' + reason) })
细心的人可能会发现,该例子中 then
第一个参数是 undefined
,且 value
值5被传到了 p1
成功时的回调函数中。上面那个例子中 then
的第二个参数是 undefined
,同样 reason
值也传到了 p1
失败时的回调函数中。这是因当对应的参数不为函数时,会将前一 promise
的状态和值传递下去。
promise
含有一个实例方法 catch
,从名字上我们就看得出来,它和异常有千丝万缕的关系。其实 catch(onReject)
方法等价于 then(undefined, onReject)
,也就是说如下两种情况是等效的。
new Promise(function(resolve, reject){ reject(new Error('error')) }).then(undefined, function(reason){ console.log(reason) // Error: error(…) }) new Promise(function(resolve, reject){ reject(new Error('error')) }).catch(function(reason){ console.log(reason) // Error: error(…) })
我们提到参数不为函数时会把值和状态传递下去。所以我们可以在多个 then
之后添加一个 catch
方法,这样前面只要 reject
或抛出异常,都会被最后的 catch
方法处理。
new Promise(function(resolve, reject){ resolve(5) }).then(function(value){ taskA() }).then(function(value){ taskB() }).then(function(value){ taskC() }).catch(function(reason){ console.log(reason) })
Promise
的静态方法
Promise
还有四个静态方法,分别是 resolve
、 reject
、 all
、 race
,下面我们一一介绍。
除了通过 new Promise()
的方式,我们还有两种创建 Promise
对象的方法:
Promise.resolve()
它相当于创建了一个立即 resolve
的对象。如下两段代码作用相同:
Promise.resolve(5) new Promise(function(resolve){ resolve(5) })
它使得promise对象直接 resolve
,并把5传到后面 then
添加的成功函数中。
Promise.resolve(5).then(function(value){ console.log(value) // 5 })
Promise.reject()
很明显它相当于创建了一个立即 reject
的对象。如下两段代码作用相同:
Promise.reject(new Error('error')) new Promise(function(resolve, reject){ reject(new Error('error')) })
它使得promise对象直接 reject
,并把error传到后面 catch
添加的函数中。
Promise.reject(new Error('error')).catch(function(reason){ console.log(reason) // Error: error(…) })
Promise.all()
它接收一个promise对象组成的数组作为参数,并返回一个新的 promise
对象。
当数组中所有的对象都 resolve
时,新对象状态变为 fulfilled
,所有对象的 resolve
的 value
依次添加组成一个新的数组,并以新的数组作为新对象 resolve
的 value
,例:
Promise.all([Promise.resolve(5), Promise.resolve(6), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) // fulfill [5, 6, 7] }, function(reason){ console.log('reject',reason) })
当数组中有一个对象 reject
时,新对象状态变为 rejected
,并以当前对象 reject
的 reason
作为新对象 reject
的 reason
。
Promise.all([Promise.resolve(5), Promise.reject(new Error('error')), Promise.resolve(7), Promise.reject(new Error('other error')) ]).then(function(value){ console.log('fulfill', value) }, function(reason){ console.log('reject', reason) // reject Error: error(…) })
那当数组中,传入了非promise对象会如何呢?
Promise.all([Promise.resolve(5), 6, true, 'test', undefined, null, {a:1}, function(){}, Promise.resolve(7) ]).then(function(value){ console.log('fulfill', value) // fulfill [5, 6, true, "test", undefined, null, Object, function, 7] }, function(reason){ console.log('reject', reason) })
我们发现,当传入的值为数字、boolean、字符串、undefined、null、{a:1}、function(){}等非promise对象时,会依次把它们添加到新对象 resolve
时传递的数组中。
那数组中的多个对象是同时调用,还是一个接一个的依次调用呢?我们再看个例子
function timeout(time) { return new Promise(function (resolve) { setTimeout(function () { resolve(time); }, time); }); } console.time('promise') Promise.all([ timeout(10), timeout(60), timeout(100) ]).then(function (values) { console.log(values); [10, 60, 100] console.timeEnd('promise'); // 107ms });
由此我们可以看出,传入的多个对象几乎是同时执行的,因为总的时间略大于用时最长的一个对象 resolve
的时间。
Promise.race()
它同样接收一个promise对象组成的数组作为参数,并返回一个新的 promise
对象。
与 Promise.all()
不同,它是在数组中有一个对象 resolve
或 reject
时,就改变自身的状态,并执行响应的回调。
Promise.race([Promise.resolve(5), Promise.reject(new Error('error')), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) // fulfill 5 }, function(reason){ console.log('reject',reason) }) Promise.race([Promise.reject(new Error('error')), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) }, function(reason){ console.log('reject',reason) //reject Error: error(…) })
且当第一个参数为数字、boolean、字符串、undefined、null、{a:1}、function(){}时,都会直接以该值 resolve
。
那么问题又来了,既然数组中第一个元素成功或失败就会改变新对象的状态,那数组中后面的对象是否会执行呢?
function timeout(time) { return new Promise(function (resolve) { setTimeout(function () { console.log(time) resolve(time); }, time); }); } console.time('promise') Promise.race([ timeout(10), timeout(60), timeout(100) ]).then(function (values) { console.log(values); [10, 60, 100] console.timeEnd('promise'); // 107ms }); // 结果依次为 // 10 // 10 // promise: 11.1ms // 60 // 100
说明即使新对象的状态改变,数组中后面的promise对象还会执行完毕,其实 Promise.all()
中即使前面 reject
了,所有的对象也都会执行完毕。规范中,promise对象执行是不可以中断的。
promise
对象即使立马改变状态,它也是异步执行的。如下所示:
Promise.resolve(5).then(function(value){ console.log('后打出来', value) }); console.log('先打出来') // 结果依次为 // 先打出来 // 后打出来 5
但还有一个有意思的例子,如下:
setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i<10000 ; i++ ){ i==9999 && resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3);
结果是 1 2 3 5 4,命名4是先添加到异步队列中的,为什么结果不是1 2 3 4 5呢?这个涉及到Event loop,后面我会单独讲一下。