js-Promise (承诺)实战

一.为什么需要promise?

开发网站的过程中,我们经常遇到某些耗时很长的javascript操作。其中,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。
通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。

Javascript 采用回调函数(callback)来处理异步编程。从同步编程到异步回调编程有一个适应的过程,但是如果出现多层回调嵌套,也就是我们常说的回调金字塔(Pyramid of Doom),绝对是一种糟糕的编程体验。于是便有了 Promises/A , Promises/A +等规范,用于解决回调金字塔问题。

Promise将原来回调地狱中的回调函数,从横向式增加巧妙的变为了纵向增长。以链式的风格,纵向的书写,使得代码更加的可读和易于维护。

去除回调金字塔

Promise历史:早在1976年就有人提出Promise 的概念。之后的计算机语言发展中,很多语言都提供了与 Promise 相关的特性。而对于Javascript语言来说,最早让大家广泛接触的 Promise 相关的库是由 jQuery.Deferred()
对象实现的。随着 Promise/A+ 标准规定了一系列 API,实现该标准的库如雨后春笋版涌现了出来,在最新的ES6中已经提供了Promise的内置对象,成为了基础库。

什么是Promise? 一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。Promise表示一个异步操作的最终结果。

  • 原生js promise 有兼容性问题。
  • jQuery1.5.0之后 封装的 deferred对象 (defer的意思是"延迟")。简单说,deferred对象就是jQuery的回调函数解决方案。兼容所有主流浏览器。推荐大家了解一下JQquery的deffered对象详解
  • ES6 Promise 对象,通过babel转码,也是可以兼容主流浏览器。

二.Promise/A+基本的规范:

  1. 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)。
  2. 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换。
  3. promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致。
  4. then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。ajax就是一个thenable对象。
promise状态变化

优点:
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

 loadImg('a.jpg', function() {  
      loadImg('b.jpg', function() {  
         loadImg('c.jpg', function() {  
            console.log('all done!');  
       });  
   });  
});  //不友好的层层嵌套

缺点:
Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

三.ES6 Promise基本的API

1. Promise.resolve() //生成一个成功的promise对象
2. Promise.reject() ////生成错误的一个promise对象
3. Promise.prototype.then() //核心部分

  • 返回一个新的Promise。

4. Promise.prototype.catch() //异常捕获
**5. Promise.all() **

  • 接收 promise对象组成的数组作为参数(Promise.all方法的参数可以不是数组,但必须具有Iterator接口)。
  • 当这个数组里的所有promise对象 全部变为resolve或遇到第一个reject状态的时候,它才会去调用 .then 方法。
  • 传递给 Promise.all 的promise并不是一个个的顺序执行的,而是 同时开始、并行执行的。

6. Promise.race() //最先执行的promise结果

  • 只要有一个promise对象进入 resolve 或者 reject 状态的话,就会调用后面的.then方法。
  • 如果有一个promise对象执行完成了,后面的还会不会再继续执行了呢? 在 ES6 Promises 规范中,也没有取消(中断)promise对象执行的概 念,我们必须要确保promise最终进入resolve or reject状态之一。所以,后面的promise对象还是会继续执行的。

四.ES6 Promise基本用法

1.创建promise对象。

  • new Promise(fn) 返回一个promise对象
  • 在 fn 中指定异步等处理。
  • 处理结果正常的话,调用 resolve(处理结果值)。
  • 处理结果错误的话,调用 reject(Error对象)。
 //示例
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://baidu.com";    
getURL(URL)
.then(function onFulfilled(value){
  console.log(value); 
})
.catch(function onRejected(error){
    console.error(error);
});
 /*其实 .catch 只是 promise.then(undefined, onRejected) 的别名
 而已, 如下代码也可以完 成同样的功能。*/

 getURL(URL).then(onFulfilled, onRejected);

总结:

  1. 用 new Promise 方法创建promise对象
  2. 用 .then 或 .catch 添加promise对象的处理函数

2. promise.prototype.then()

  • 它的作用是为Promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。

  • then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法。

** 3.Promise.prototype.catch() **

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

getAjax('url/info').then(function(data) {
  // ...
}).catch(function(error) {
  // 处理 ajax 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

总结:
1.上面代码中,getAjax方法返回一个 Promise 对象,如果该对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
2.Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

有了then里面的第二个onRejected函数捕获错误,为什么还需要catch?

function throwError(value) { // 抛出异常
    throw new Error(value);
}
// <1> onRejected不会被调用
function main1(onRejected) {
  return Promise.resolve(1).then(throwError, onRejected);
}

// <2> 有异常发生时onRejected会被调用
function main2(onRejected) {
    return Promise.resolve(1).then(throwError).catch(onRejected);
}
  //执行main函数
main1(function(){
    console.log("错误异常");
}
  //执行main2函数
main2(function(){
    console.log("错误异常");
}
/*Promise.prototype.catch方法是.then(null, rejection)的别名,
  用于指定发生错误时的回调函数。

  一般来说,不要在then方法里面定义Reject状态的回调函数(即
    then的第二个参数),总是使用catch方法。
*/
Promise.resolve(1).then(throwError).then(null, onRejected);

在函数main1因为虽然我们在的第二个参数中指定了用来错 误处理的函数,但实际上它却不能捕获第一个参数 指定的函数(本例为throwError )里面出现的错误。
与此相对的是main2中, 的代码则遵循了 throwError → onRejected 的调用流程。 这时候 中出现异常的话,在会被方法链中的下一个方法,即 .catch 所捕 获,进行相应的错误处理。

总结:
.then 方法中的onRejected参数所指定的回调函数,实际上针对的是其promise对象或者 之前的promise对象,而不是针对 方法里面指定的第一个参数,即onFulfilled所指 向的对象,这也是 then 和 catch表现不同的原因。

4.Promise.resolve()

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
该函数的参数四种情况:
(1)参数是一个Promise实例,那么Promise.resolve将不做任何操作,原封不动的将实例返回。
(2)参数是一个thenable对象,会将其转为Promise对象,然后立即执行该对象的then方法。
(3)参数不是具有then方法的对象,或根本就不是对象。比如说字符之类,则Promise.resolve方法返回一个新的Promise对象,并且状态Resolved。
(4)不带有任何参数,直接返回一个状态为Resolved的Promise对象。

  • 使用Promise.resolve()创建Promise对象
/*静态方法 Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。*/
//比如 
Promise.resolve(1)
.then(function(value){
    console.log(value);
});  
//可以认为是以下代码的语法糖。
new Promise(function(resolve){ 
  resolve(1);
})
.then(function(value){
    console.log(value);
});
// 控制台输出1
注意: 无论Promise.resolve的参数是什么,只要变成了rejected,或者resolved。都会执行then里面的resolve函数。
  • 将 thenable 对象转换为promise对象。 什么是thenable对象?
    简单来说它就是一个非常类似promise的东西。thenable指的是一个
    具有 .then 方法的对象。jQuery.ajax(),这个对象具有 .then 方法。
阮一峰ES6代码示例 thenable

5.Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

Promise.reject('这是错误的信息').then(function(){

},function(res){
     console.log(res); //这里是错误信息  
});
 注意: 无论Promise.reject的参数是什么,只要变成了rejected,或者resolved。都会执行then里面的reject函数。

6.Promise.all()

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。如果传入的不是不是Promise对象就会调用Promise.reslove()方法将其转换成Promise实例。

Promise.all()示例
//错误了,5   
//如果是全部resolved,返回的是一个数组[true,3,5]

7.Promise.race()

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。

promise.race()
//one one two 所以后面的promise对象肯定会继续执行 
// 第一个one是在p1里面打印出来的

五.Promise只能进行异步操作?

Promise在规范上规定 Promise只能使用异步调用方式 。

 // 可以看出promise是 一个异步函数  
  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

why?
因为同步调用和异步调用同时存在容易导致一些混乱。举个类似的例子。

function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
           fn();
    } else {
         window.addEventListener('DOMContentLoaded', fn); 
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

如上js函数会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调 用还是异步调用。因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会 不同。为了解决这个问题,我们可以选择统一使用异步调用的方式。

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==');

六.实际开发使用示例

jQuery-deferred应用
ajax-统一管理
引用实例
ES6-Promise对象应用
jquery deferred多个异步任务的串行处理
jquery deferred多个异步任务的并行处理

7. promise实现剖析

Promise是具有链式操作,因此Promise的内部结构应该是一个单向链表结构,每个节点除了自身数据外,还有一个字段用于指向下一个Promise实例。

7.1promise基本标准的相关实现

// Promise构造函数接收一个函数,函数执行完同步或异步操作后,调用它的两个参数resolve和reject

var promise = new Promise(function(resolve, reject) {
  /*
    如果操作成功,调用resolve并传入value
    如果操作失败,调用reject并传入reason
  */
});

实现构造函数

function Promise(executor) {
  var _this = this
  _this.status = 'pending' // Promise当前的状态
  _this.data = undefined  // Promise的值
  _this.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
  _this.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
  
//resolve
  function resolve(value){
    setTimeout(function(){ //异步执行
      if(_this.status == 'pending'){
          _this.status = 'resolved';
          _this.data = value;
      }
    for(var i=0;i< _this.onResolvedCallback.length;i++){
      _this. _this.onResolvedCallback[i](value);
    }
   });
  }

  //reject
  function reject(reason){
    setTimeout(function(){ //异步
      if(_this.status == 'pending'){
          _this.status ='rejected';
          _this.data = reason;
      }
      for(var i=0;i< _this.onRejectedCallback.length;i++){
        _this.onRejectedCallback[i](reason);
      }
    });
  }

  try {
      executor(resolve, reject) // 执行executor并传入相应的参数
  }catch(e){
      reject(e);
  }
}//end

实现then:链式调用

// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调

Promise.prototype.then = function(onResolved,onRejected){
  var _this = this;
  var promise2;
   // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理   实参留空 且让值可以穿透到后面
  onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value;}
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {return reason;}
  
  //Promise 里面有三种状态,我们分三个if来处理,在里面分别返回一个new Promise。
  //根据标准我们知道,promise2的值取决于then里面函数的返回值。

 /* promise2 = promise1.then(function(value) {
      return 4
  }, function(reason) {
      throw new Error('sth went wrong')
  })*/

   if (_this.status === 'resolved') {
      //此时this的状态已经是resolved ,所以我们这里执行onResolved
      return promise2 = new Promise(function(resolve, reject) {
         try{
           var x = onResolved(_this.data)
           if (x instanceof Promise) { 
            // 如果onResolved的返回值是一个Promise对象,
            //直接取它的结果做为promise2的结果
                x.then(resolve, reject)
            }
            resolve(x) // 否则,以它的返回值做为promise2的结果
        }catch(e){
            // 如果出错,以捕获到的错误做为promise2的结果
           reject(e) ;
        }
         
      })
    }

  if (_this.status === 'rejected') {
      //此时this的状态已经是rejected ,所以我们这里执行onRejected
     return promise2 = new Promise(function(resolve, reject) {
          try {
              var x = onRejected(self.data)
              if (x instanceof Promise) {
                  x.then(resolve, reject)
              }
          } catch (e) {
              reject(e)
          }
      })
    }

  if (_this.status === 'pending') {
   // 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,
   // 只能等到Promise的状态确定后,才能确实如何处理。
  // 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入this的回调数组里
    return promise2 = new Promise(function(resolve, reject) {
        
          _this.onResolvedCallback.push(function(value) {
              try {
                var x = onResolved(_this.data)
                if (x instanceof Promise) {
                    x.then(resolve, reject)
                }
              } catch (e) {
                 reject(e)
              }
          });
          
         _this. onRejectedCallback.push(function(resaon) {
              try {
                var x = onRejected(_this.data)
                if (x instanceof Promise) {
                    x.then(resolve, reject)
                }
              } catch (e) {
                 reject(e)
              }
          });

    })
  }
 }

实现catch

  Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected)
}

源码:https://github.com/components/es6-promise

如果不对之处,请指正哦~谢谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 11,023评论 26 95
  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 2,127评论 0 12
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,348评论 0 19
  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 7,295评论 6 19
  • 大学时,我还记得老师的第一句话是告诉我们珍惜仅有的大学时光,在学校里交的朋友是真正的可以一辈子的,那时我小嘴一撇,...
    喂饱阅读 114评论 0 0