深入解析ES6中的promise
es6中的promise对象很早就听说过,据说是为了解决我们使用回调产生回调地狱的问题。今天下午既然有这么想学的欲望,就来看一看吧,当然参考的还是阮一峰老师的教程。
第一部分:什么是promise
看本文的最后一个例子,迅速理解。
promise是es6中的一个内置的对象(实际上是一个构造函数,通过这个构造函数我们可以创建一个promise对象),它是为了解决异步问题的。promise的英文意思是承诺。
promise的特点如下:
•promise有三种状态:pending(进行中)、resolved(已完成)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这就是promise。
•promise一共有三种状态,但是他们之间是如何转化的呢? 其一: 从pending(进行中)到resolved(完成)。其二: 从pending(进行中)到rejected(已失败)。 且只有这两种形式的转变,即使是promise对象的结果也无力回天了。
但是promise也是有一定的缺点的,如在pengding时,我们无法取消状态,另外,我们没法判断pending究竟是刚刚开始的pending还是即将要完成的pending。
第二部分:使用promise
前言:
在下面例子的讲解中,我们需要使用到settimeout函数,这里首先说明settimeout的一些重要用法。一般我们常用的如下所示:
settimeout(func(){}, 1000);
即在1000ms之后执行func函数,但是其实settimeout还可以传入更多的参数。这篇博客做了讲解,而这里为了了解下面的例子我们只需要知道,在chrome中可以传入第三个、第四个参数....作为func()函数的参数传递进去,举例如下:
settimeout(function (a, b) { console.log(a + b); }, 1000, 20, 50);
最终的输入结果是:70
下面的代码创建了一个promise实例:
var promise = new promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
其中,由于promise是构造函数,所以我们使用new来创建一个对象即可, 值得注意的是:function(resolve, reject){}这个函数是必须要写的,否则就不是promise了。
这个函数是为了初始化promise对象,其中这个函数接受了两个函数作为参数, 如果在函数体中我们执行了resolve函数,那么promise的状态就会由pending转化为resolved(或fullfilled,两者是相同的),类似的,如果我们执行了reject函数,pending就会变成 rejected。
注意: 这个例子的if语句不是必要的,这里想要表达的意思是如果得到了异步成功的相关结果,我们就将调用resolve,将pending转化为resolved,并且将异步成功后的value值传递进去以便后面使用,说是以便后面使用是因为promise还有一个then()方法,即可以定义在异步成功(或失败)之后需要做的事情。这也就是resolve和reject内置函数存在的意义了。
当创建了这个promise对象之后,就一定会有一个结果了,但是成功和失败还是不确定的,我们需要根据判断结果的成功和失败来做不同的事情,于是用到了then()方法,如下所示:
promise.then(function(value) { // success }, function(error) { // failure });
下面是一个例子,并做了详尽的说明:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>testsettimeout</title> </head> <body> <script> var promise = new promise(function (resolve, reject) { console.log("good"); var a = 10; var b = a + 25; if ( b === 35 ) { // 一旦异步执行成功,我们就调用内置的 resolve函数,将pending状态转化为resolved,并且传入我们希望传出的执行成功后的结果。 // 注意: 这里一旦状态转变,那么后面就一定会调用then方法中的第一个参数的函数,然后将我们传入给resolve的参数传给then方法中的第一个方法作为参数,我们就可以在then的第一个方法中使用了。 resolve(b); } else { reject("异步执行失败"); } }); promise.then(function (value) { console.log("异步执行成功,输出执行结果:" + value); }, function (error) { console.log("异步执行失败,输出执行结果:" + error); }); </script> </body> </html>
而阮一峰老师所列的下面的这个例子可以清楚的看出promise就是异步的:
let promise = new promise(function(resolve, reject) { console.log('promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('hi!');
最终的输出结果是: promise resolved hi
分析: 从这个例子中可以看出promise的异步,因为前面的两部分代码还没有执行,就已经输出了hi。另外可以确定的是 resolved 一定是在 promise之后输出的,这个顺序是不可能有问题的。
下面的例子是一个异步添加图片的url的例子
function loadimageasync(url) { return new promise(function(resolve, reject) { var image = new image(); image.onload = function () { resolve(image); }; image.onerror = function() { reject(new error('could not load image at' + url)); } image.src = url; }); }
如果加载成功就使用resolve方法,如果失败就使用reject方法。
下面的例子是阮一峰老师封装的ajax的例子,是在太好,没法不直接拿来参考~
var getjson = function(url) { var promise = new promise(function(resolve, reject){ var client = new xmlhttprequest(); client.open("get", url); client.onreadystatechange = handler; client.responsetype = "json"; client.setrequestheader("accept", "application/json"); client.send(); function handler() { if (this.readystate !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new error(this.statustext)); } }; }); return promise; }; getjson("/posts.json").then(function(json) { console.log('contents: ' + json); }, function(error) { console.error('出错了', error); });
这里应该大家都可以看懂,值得注意的是:handler这个处理函数的使用在这里显得很巧妙。
第三部分: promise.prototype.then()
在上一部分,我们实际上已经介绍了then()方法,而这里需要强调的有两点。
第一: then()方法是promise原型上定义的方法。
第二:then()方法支持链式调用,上一个then()方法调用后返回的结果会传给下一个then方法中。
第一:我们再chrome中输入 promise.prototype可以看到下面的例子:
可以看出在promise的原型中确实是由then方法的。(注:比如我们想看array这个内置对象有哪些方法,我们就可以直接在chrome中输入array.prototype,然后就可以看到对应方法的列表了)
第二: then()的作用是为promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then()由于支持链式调用,所以也可以写成下面这样:
getjson("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });
第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
看下面的这里例子:
getjson("/post/1.json").then(function(post) { return getjson(post.commenturl); }).then(function funca(comments) { console.log("resolved: ", comments); }, function funcb(err){ console.log("rejected: ", err); });
即第一个then又返回了一个promise,如何这个promise的状态变成了 resolved,那么就会执行第二个then的第一个函数, 如果变成了 rejected,就会执行第二个第二个函数。
第四部分: promise.prototype.catch()
promise.prototype.catch()方法实际上是then(null, rejection)方法的别名, 这里使用catch()纯粹是为了便于使用和理解。
getjson('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 处理 getjson 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); });
在之前的例子中,我们讲解then()方法接受两个参数,第一个参数是pending变成resolved之后执行的函数,它是必选的; 第二个参数是pending变成rejected之后执行的函数,它是可选的。
我们建议,最后不要使用第二个参数,取而代之我们最好使用catch(),如下所示:
// bad promise .then(function(data) { // success }, function(err) { // error }); // good promise .then(function(data) { //cb // success }) .catch(function(err) { // error });
值得注意的是:catch()方法返回的还是一个promise对象,我们可以在后面继续使用then进行链式调用。
第五部分:promise.all()
promise.all()方法用于将多个promise实例包装成一个新的promise实例,如下所示:
var p = promise.all([p1, p2, p3]);
其中的p1,p2,p3都是promise对象实例,如果不是,就会先调用下面讲到的promise.resolve方法,将参数转为promise实例,再进一步处理。
p的状态由p1,p2,p3决定, 分成下面的两种情况:
•只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
•只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
让我们看看下面的具体的例子:
// 生成一个promise对象的数组 var promises = [2, 3, 5, 7, 11, 13].map(function (id) { return getjson("/post/" + id + ".json"); }); promise.all(promises).then(function (posts) { // ... }).catch(function(reason){ // ... });
在这个例子中,通过数组的map生成了6个promise对象,然后作为参数传递给了promise.all() 只有这6个promise对象最终返回的都是 resolved时, 才会调用promise.all()后面then()方法。
第六部分:promise.race()
该方法同样是将多个promise实例包装成一个新的promise实例,如下:
var p = promise.race([p1, p2, p3]);
在上面的代码中,如果有一个promise率先改变状态,p的状态就会跟着改变。
第七部分: promise.resolve()
如果我们希望将一个现有对象转化为promise对象,我们就可以使用promise.resolve()方法。根据参数的不同可以分为4种情况:
(1)参数是一个promise实例
如果参数是promise实例,那么promise.resolve将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
如果参数是一个thenable对象,即具有then()方法的对象,那么promise.resolve()就会将该对象立即转化成 promise对象那个,并立即执行then方法。
(3)参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则promise.resolve方法返回一个新的promise对象,状态为resolved。
(4)不带有任何参数
第八部分: promise.reject()
promise.reject(reason)方法也会返回一个新的 promise 实例,该实例的状态为rejected。
第九部分: 理解promise
它的作用就是如果前面的一段代码非常的耗时,就会阻塞后面的某个代码片段。 所以我们希望他不要阻塞后面的代码,那么就让他异步好了。
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>testsettimeout</title> </head> <body> <div class="wrap"> </div> <script> console.time("time"); for (var i = 0; i < 50000; i++) { var li = document.createelement("li"); document.queryselector(".wrap").append(li); } console.timeend("time"); var a = 10; var b = a + 25; console.log( a); console.log(b); console.timeend("time"); </script> </body> </html>
最后的输出结果是:
可以看到前面的代码在阻塞后面的代码。 导致后面的代码无法运行。
因为我们希望这个耗时的代码越快执行越好,会非常严重的影响用户体验,所以把它放在了最前面,如果它没有什么用,就放在了最后面了。
但是后面的代码我们也希望尽快执行啊,那么该怎么办呢? 如果可以让前面的代码单独在一个线程上运行, 而不影响后面的代码运行就好了。
settimeout()就可以做到,如下所示:
console.time("time"); settimeout(function() { for (var i = 0; i < 50000; i++) { var li = document.createelement("li"); document.queryselector(".wrap").append(li); } console.timeend("time"); }, 0); var a = 10; var b = a + 25; console.log(a); console.log(b); console.timeend("time");
最终的输出结果如下:
可以看到在settimeout中的函数一开始就执行了。
但是为什么settimeout的内容执行的这么慢呢? 我们再看一个例子
console.time("time"); settimeout(function() { console.timeend("time"); }, 0);
结果如下:
time: 2.234ms
即 settimeout 自身就会先消耗一定的时间。
ok,那么说了半天promise到底怎么和他们比较呢? 他的价值在哪里呢?
价值都在这里了!看下面的这个例子:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>testsettimeout</title> </head> <body> <div class="wrap"> </div> <script> let promise = new promise(function(resolve, reject) { console.log('promise! 我是promise对象里的函数,我最早出现是因为我要给后面一个耗时的家伙提供数据a'); var a = 20; resolve(a); }); promise.then(function(value) { console.log('哈哈哈,我得到数据a了,要开始执行喽。我是一个非常耗时的操作,依赖前面的数据,但是我想快一点执行,这有利于用户体验,所以别把我放在后头啊;我被放在了一个新的线程上,不会阻塞别人的'); for (var i = 0; i < 1000000; i++) { var li = document.createelement("li"); document.queryselector(".wrap").appendchild(li); } console.log("执行完毕"); }); console.log('hi! 我是什么都不依赖的程序,但我也想快一点执行,不能委屈我啊'); </script> </body> </html>
说明: 在已经执行过的情况下,录制屏幕,然后按下刷新键,可以发现,在new promise里的函数是立即执行的,紧接着是promise.then()之后的函数立即执行,而没有等待then()函数,最后进入then()函数,说了一堆废话,最后大约2 ~ 3s之后执行结束。故promise.then()函数才是真正的异步执行。
promise实现:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>promise</title> </head> <body> <script> function mypromise(fn) { this.state = 'pending'; this.value = void 0; this.donelist = []; this.faillist = []; // 在创建一个promise的时候,需要传递一个fn函数,接受参数resolve和reject。 // resolve和reject目前还没有定义,因为用户是直接调用的,如resolve('success'),所以这个resolve方法是需要我们自己来定义的。 var self = this; function resolve(value) { // 一旦resolve,状态机的状态由pending -> resolved // 并且在resolve的时候状态必须是pending,才能转换状态。 // 异步执行所有回调函数 settimeout(function () { if (self.state == 'pending') { self.state = 'resolved' self.value = value; // 一旦成功,我们就可以执行所有promise上挂载的 donelist 中所有的回调函数了,并将value值传递过去 for (var i = 0; i < self.donelist.length; i++) { self.donelist[i](value); } } }, 0); } function reject(reason) { // 一旦reject,状态机的状态由pending -> rejected // 在reject的时候状态必须是pending,才能转化状态。 // 异步执行所有回调函数 settimeout(function () { if (self.state == 'pending') { self.state = 'rejected' self.value = reason; // 一旦失败,我么就可以把所有的 faillist 调用了,并且传递 reason for (var i = 0; i < self.faillist.length; i++) { self.faillist[i](reason); } } }, 0); } fn(resolve, reject); } mypromise.prototype = { constructor: mypromise, // then方法接受成功时的回调和失败时的回调 // 实际上,这里的then方法就像路由中的注册路由一样,是一个注册的过程,而没有真正的调用。 // 并且then是支持链式调用的,所以then应该返回一个promise。 如果返回旧的,那么因为状态不能改变,所以没有意义,所以我么一定要返回一个新的promise,这样这个状态机才可以再次正常的工作。 then: function (onresolved, onrejected) { var self = this; // 对于then而言,最终要返回一个新的promise,这样才支持链式调用。 var promise2; // 这里要做一个判断,看传递进来的是否是一个函数,如果是,则不变,如果不是,就拒绝。 // 如果是promise.then().then().then(function (value) {}); 我们还希望拿到value,那么就要把value在即使没有用到onresolved的时候也传递下去。对于reason也是如此。 onresolved = typeof onresolved == 'function' ? onresolved : function (value) {return value;} onrejected = typeof onrejected == 'function' ? onrejected : function (reason) { return reason;} // 下面这一部分是比较核心的内容,因为then最终要返回一个promise,但是,这个promise究竟应该怎么返回呢? 如果在then中,用户就返回了promise,那么我们就用户的,如果用户用的不是promise,那么我么就要自己封装好这个promise了。 // 注意: promise也是需要异步调用的,所以可以使用promise进行封装。 switch(this.state) { case 'resolved': // 如果resolved,则返回一个新的promise return promise2 = new mypromise(function (resolve, reject) { settimeout(function () { try { // 这里相当于直接就给执行了,看看返回的是什么值,如果是promise,那么直接就使用这个promise了。 var x = onresolved(self.value); if (x instanceof mypromise) { x.then(resolve, reject); } } catch (e) { reject(e); } }, 0) }) case 'rejected': // 如果rejected,同样也需要返回一个新的promise return promise2 = new mypromise( function (resolve, reject) { settimeout(function () { try { var x = onrejected(self.value); if (x instanceof mypromise) { x.then(resolve, reject); } } catch (e) { reject(e); } }, 0); }) // 如果是pending状态,我们就不能确定使用什么,等到状态确定之后才能决定。 case 'pending': return promise2 = new mypromise(function () { // 注意:一般then都是执行的这里, 即在then的时候进行注册,把相应的成功的和失败的都会初测在这里,push的是一个函数,所以这里的onresolved还是没有执行的。 settimeout(function () { self.donelist.push(function (value) { try { var x = onresolved(self.value) if (x instanceof mypromise) { x.then(resolve, reject) } else { onresolved(value); } } catch (e) { reject(e) } }); console.log(self.donelist) self.faillist.push(function (value) { try { var x = onrejected(self.data); if (x instanceof mypromise) { x.then(resolve, reject); } } catch (e) { } }); }, 0); }) default: console.log(this.state); return; } }, catch: function (onrejected) { return this.then(null, onrejected); } } var promise = new mypromise(function (resolve, reject) { if (5 > 3) { resolve('success'); } }); promise.then(function (value) { console.log('哈哈哈哈或'); }); </script> </body> </html>
总结
以上所述是小编给大家介绍的es6中的promise,希望对大家有所帮助