Promise 学习笔记

回调地狱

首先有一个需求,如何连续根据函数的依赖关系,实现多个函数的连续调用,而且要在前置函数完成的情况下。例如 1 秒钟之后执行 fn1fn1 执行完毕,相隔 1 秒,执行 fn2fn2 执行完毕,相隔 1 秒,执行 fn3

我们可以利用回调函数,将后续需要执行的函数作为前置函数的回调函数参数,在前置函数执行之后执行。

// exp 1
function fn1(callback) {
  setTimeout(()=>{
    console.log('fn1 executed');
    callback();
  },1000);
}

function fn2(callback) {
  setTimeout(()=>{
    console.log('fn2 executed');
    callback();
  },1000);
}

function fn3() {
  setTimeout(()=>{
    console.log('fn3 executed');
  },1000);
}

fn1(function() {
  fn2(function() {
    fn3();
  });
}); 
// "fn1 executed"
// 1s~
// "fn2 executed"
// 1s~
// "fn3 executed"

上述代码中不断嵌入回调函数,回调函数中还有函数作为参数,结果输出没有问题。但是代码缺乏可读性和拓展性,健壮性。当其中一个函数需要修改,或者嵌套回调层数增多,将陷入常说的“回调地狱”中,我们需要一种更为符合逻辑,更优雅的异步回调的方法—— Promise。

Promise 的含义
MDN 文档 中,它是被这样定义的:

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 Promise 对象

阮一峰老师的 理解

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise 通过对构造函数的设计,让异步编程比传统的方式更合理,处理更为逻辑化,代码也更加优雅。

语法

Promise 对象具有两个特点:1)Promise 对象分别有三种状态 pendingfullfiledrejected。最开始时是pending,当内部异步函数执行成功,则状态立即变为fullfilled且不可更改;当内部异步函数执行失败,则状态立即变为rejected,且不可更改。2)Promise 对象定义后会立即执行,而 resolve 函数要等待响应的结果。

// exp 2
const promiseExp  = new Promise(function(resolve, rejected) { 
  if(/*success condition */) {
    resolve(value);
  }else {
    reject(error);
  }
});

function ifSuccess() {
  // do somthing if success
}

function ifFailure(error) {
  // do somthing if fail
}
promiseExp().then(ifSuccess).catch(ifFailure(error));

上述代码中,当 resolve 或者 reject 被执行,Promise 状态从pending(进行中) 变成fullfilled(完成)或者 rejected(失败);当其中任意一个发生时,就会调用相对应的函数,成功后该执行的函数或失败后该执行的函数,且由于.then().catch方法依旧返回一个 Promise 对象,::因此可以链式调用,类似于解决文章开头的“回调地狱”的问题。
Promise 改造:

// exp 3
function fn1() {
  return new Promise(function(resolve, reject) {
    console.log('fn1 promise immediatly');
    setTimeout(()=>{
      console.log('fn1 async');
      resolve();
    },1000);
  });
}

function fn2() {
  return new Promise(function(resolve, reject) {
    console.log('fn2 promise immediatly');
    setTimeout(()=>{
      console.log('fn2 async');
      resolve();
    },1000);
  });
}

function fn3() {
  return new Promise(function(resolve, reject) {
    console.log('fn3 promise immediatly');
    setTimeout(()=>{
      console.log('fn3 async');
      resolve();
    },1000);
  });
}

function onerror() {
  console.log('error');
}

fn1().then(fn2).then(fn3).catch(onerror);
console.log('outer after calling');
/*
"fn1 promise immediatly"
"outer after calling"
1s~
"fn1 async"
"fn2 promise immediatly"
1s~
"fn2 async"
"fn3 promise immediatly"
1s~
"fn3 async"
*/

上述代码对最初的代码进行了改造,我们可以看到几点:
1)通过状态的变化,触发.then()函数中的函数,我们避免了多层的回调函数嵌套,以同步的方式进行异步函数回调,更具有可读性,合理性和健壮性。
2)Promise 对象是立即执行的,体现在1s的间距,"fn1 immediatly" 是和 “outer after calling ”一起输出的,而其他的都是上一个异步函数(fnx async )和 异步完成调用函数(fnx+1 promise immediatly)一起输出的。
3).then()返回的仍是一个 Promise 对象,因此在连续的回调函数依赖关系中,通过对 promise.prototype.then 的连续链式调用,实现了连续的函数回调(如下图)。

Promise 的链式调用

Promise 的原型

Promise.prototype.then()

添加解决(fulfillment)和拒绝(rejection)回调到当前 Promise, 返回一个新的 Promise, 将以回调的返回值来resolve

then(onfulfilled, onrejected)then()有两个参数,一个是 resolve 状态的回调函数,一个是 reject状态的回调函数(可选),分别对应onfullfilledonrejected;根据上面的例子,then返回的仍是 Promise 对象,因此可以实现链式调用。

// exp 4
getIp("/getIp.php").then(
  ipResult => getCity(ipResult.ip)
).then(
  city => getWeather(city),
  err => console.log('rejected: ' + err);
)

上面的代码中,第一个then()指定的回调函数getCity返回的仍是一个 Promise 对象,因此继续调用then(),此时,如果第二个then指定的回调函数就会等待新的返回的 Promise 对象状态的变化,如果是状态变为 resolved,则执行getWeather,如果是rejected,则执行console.log('rejected: '+err);

Promise.prototype.catch

then()一样,catch()方法返回的是一个 Promise 对象,以及他拒绝的理由,他的行为和Promise.prototype.then(undefined, onRejected)。实际上,ECMA 的 官方文档 就是这么写的:obj.catch(onRejected)等同于obj.then(undefined, onRejected)

promise.prototype.catch from ECMA

catch 的使用中,他不仅可以捕获来自源 Promise 对象抛出的错误(下面第一个例子),也同时可以捕获在链式调用thencatch时,由then抛出的错误(第二个例子)。

// exp 5
const promise = new Promise(function(resolve, reject) {
  console.log('before throw');
  throw new Error('error test');
  console.log('after throw');
});
promise.catch(function(error) {
  console.log(error);
});
//  "before throw"
//  "error test"

上面的代码中,我们定义了一个 Promise 对象,他的作用就是抛出一个错误,并将错误的内容传递出去,当 Promise 对象调用catch捕获的时候,它可以直接捕获由 Promise 传递出的 error,并且立即执行完毕throw,错误被捕捉之后,就不再执行之后的函数,因此在throw之后的log语句没有被执行出来。

// exp 6
const promise = new Promise(function(resolve,reject) {
  resolve('error test');
});

promise.then(function(err){
  console.log('now I throw an error');
  throw new Error(err);
}).catch(function(err){
  console.log(err);
});
//  "now I throw an error"
//  "error test"

上面的代码和前一段不同,在 Promise 对象中,并没有抛出错误。错误时在then的回调函数中抛出的,可以看到,catch不仅可以捕获来自第一个 Promise 的错误,由于链式调用的原因,还可以捕获then()回调函数返回的 Promise 对象的错误。

前面说道:

Promise 对象具有两个特点:1)Promise 对象分别有三种状态 pendingfullfiledrejected。最开始时是 pending,当内部异步函数执行成功,则状态立即变为fullfilled且不可更改;当内部异步函数执行失败,则状态立即变为rejected且不可更改

如果是已经执行resolve之后,状态变成了fullfilled,再抛出错误,会不会被catch捕获呢?

// exp 7
const promise = new Promise(function(resolve,reject) {
  resolve('The ink is dry');
  throw new Error('An error after resolve');
});

promise.then(function(msg){
  console.log(msg);
}).catch(function(err){
  console.log(err);
});
// "The ink is dry"

不会,因为 Promise 的状态已经从pending变成fullfilled,就不会改变,同理如 exp 5 的 Promise 对象中的 console.log语句,在throw语句之前的log被执行了,之后的log没有被执行,因为throw之后,Promise 的状态已经改变了,就不会再继续执行下面的代码。

Promise.prototype.finally

finally()方法返回一个 Promise。在 Promise 结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()catch()中各写一次的情况。

// exp 8
promise()
.then(val =>{/*  do something success */})
.catch(err =>{/*  do something fail */})
.finally(() => {/*  do something whatever*/})

上面使用的案例中,我们规定了成功该做什么是,并传入一个val值,规定了失败该做什么时,并传入错误原因,最终,我们无论成功失败,都要完成的事情,它并没有输入的参数,也无法从它确定Promise 的状态。

阮一峰老师试着实现了了finally函数

// exp 9
Promise.prototype.finally = funtion (callback) {
  let P  = this.constructor;
  return this.then(
    val => P.resolve(callback()).then( () => val),
    err => P.resolve(callback()).then( () => err)
  );
}

Promise 的方法

Promise.all

.all方法的参数是一个 Promise 对象列表,而返回的仍是一个Promise 对象,当输入的所有的 Promise 对象状态都为resolved时,返回的 Promise 新对象才返回resolve,当有一个出现reject时,则新返回的 Promise 返回reject,错误原因是第一个出现失败的 Promise 的结果。

// exp 10
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

上面代码中,promise.all等待输入的三个 Promise 均完成后,尽管一些 Promise 没有包含异步函数,但是结果其结果仍然被放进了最终返回的 Promise 中。

在作为参数列表的 Promise 对象中,如果他有自己的catch函数,当他抛出错误时,他的错误将被自己的catch捕获,而不会被promise.allcatch捕获。

//  exp 11
let p1 = new Promise(function(resolve, reject) {
  resolve('p1 is ok');
}).then(result => result);

let p2 = new Promise(function(resolve, reject) {
  throw new Error('error test');
}).then(function(msg) {
  console.log(msg);
}).catch(function(err){
  console.log('p2 err captrue: '+ err);
});

let promise = Promise.all([p1,p2]);
promise.then(function(msg){
  console.log('promiseAll msg: '+msg);
}).catch(function(err){
  console.log('promiseAll err captrue: '+err);
});
/*
"p2 err captrue: Error: error test"
["p1 is ok", undefined]
*/

上面的代码中,p1 状态为resolved,并将resolve的值"p1 is ok"作为结果传入返回的回调函数中;p2 则抛出了一个错误,但是这个错误被 p2 本身的catch函数捕捉到了,catch函数捕捉到之后,返回一个新的 Promise,此时这个 Promise 的状态是resolved,因此,当使用 Promise.all([p1, p2])的时候,两者的状态都为resolved,只是 p2 没有返回的值,因此输出中,p2 的值是"undefined",如果 p2 没有自己的catch方法,则在Promsie.all([p1,p2])中则会调用catch方法。

Promsie.race

Promise.racePromise.all方法输入的参数一致,都是一个参数数组,只是.race是一旦参数数组中的某一个 Promsie 完成(resolve)或者拒绝(reject),状态更改,他就会返回一个新的Promsie,状态和参数列表中的第一个发生状态改变的 Promsie 一致。

//  exp 12
var p1 = new Promise(function(resolve, reject) {
  setTimeout(resolve,100,'promise one 100ms');
});

var p2 = new Promise(function(resolve, reject) {
  setTimeout(resolve,200,'promise two 200ms');
});

var promise = Promise.race([p1,p2]).then(function(result){
  console.log(result);
});
// ""promise one 100ms""

上面的代码中,设置了两个 Promise 对象参数,但是设置了不同的异步完成的时间,p1 比 p2 快 100ms,因此在 p1 状态发生改变,从pendingresolved之后,Promise.race立即返回新的Promise对象,状态和 p1 一直,传递的值就是 p1 的值。

Promise.resolve

Promise.resolve方法返回一个给定解析值的 Promise 对象,也就是将现有对象转化为 Promise 对象。传入的参数可以是一个 Promise 对象,也可以是一个 thenable

静态使用 resolve方法

//  exp 13
Promise.resolve('resolve exp').then(function(msg){
  console.log(msg);
},function(err){
  console.log('Error:'+err); // 不会执行
});
// "resolve exp"

上面代码中,resolve 方法直接返回一个新的 Promise 对象,并且处于 fullfilled 状态,携带的 value是 "resolve exp" 因此,新的 Promise 对象直接调用.then方法。

参数是一个thenable对象

//  exp 14
let thenable = {
  then:function(resolve, reject) {
    resolve('resolved before throw');
    reject('after resolve');
  }
};

var p = Promise.resolve(thenable);
p.then(function(msg) {
  console.log(msg);
},function(err){
  console.log('error: ' + err);
});

上面的代码中,resolve输入的是一个 thenable对象,resolve方法会将这个对象转为 Promise 对象,然后执行thenable对象的then方法,执行后p的状态将变为resolved,因此pthen方法将会被执行,输出thenable传递的msg

需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

//  exp 15
setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');
/*
"one"
"two"
"three"
*/

上面的代码中,setTimeout()是在下一轮时间循环开始时执行,Promise在本轮时间循环结束时执行,console.log立即执行,因此最先输出。

Promise.reject

本方法返回一个带有拒绝原因的Promise对象,该对象的状态自然为rejected

静态使用reject方法

// exp 16
Promise.reject('reject exp').then(function(reason) {
  console.log(reason)}, 
function(reason) {
  console.log('Error: ' + reason) ;
});
//Error: reject exp

上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

resolve方法不同的是,reject方法传入的参数,会作为后续方法的理由,而不是像resolve一样,将原 thenable 传入的参数传递。

// exp 17
let thenable = {
  then:function(resolve, reject) {
    reject('reject exp');
  }
};

var p = Promise.reject(thenable);

p.then(null,function(e){
  console.log('target: ' + e);
});
// "target: [object Object] "

上面函数中,传递到p.then中的参数不是"reject exp"字符串,而是thenable对象本身。

await async

await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用。使用 Promise 配合 await 和 async,我们已经可以像书写同步函数那样书写异步函数。

// exp 18
function resolve2second(x) {
  return new Promise(resolve => {
    setTimeout(()=>{
      resolve(x);
    },2000);
  });
};

async function fn1() {
  console.log('fn1 immediatly');
  var x = await resolve2second(10);
  console.log(x)
}

fn1();
/*
"fn1 immediatly"
// 2s~
10
*/

上面代码中,resolve2second是一个2秒后执行的异步函数,在async 函数fn1中,设置了await表达式,使得x变量赋值的操作暂停,等待Promise结果出来后,由返回的resolve值再执行对x的赋权,而fn1函数的内的console.log函数不受影响,随fn1立即执行

参考阅读

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

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,689评论 1 56
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,348评论 0 19
  • 一、Promise的含义 Promise在JavaScript语言中早有实现,ES6将其写进了语言标准,统一了用法...
    Alex灌汤猫阅读 816评论 0 2
  • 目录:Promise 的含义基本用法Promise.prototype.then()Promise.prototy...
    BluesCurry阅读 1,488评论 0 8
  • 1. Promise 的含义 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个...
    ROBIN2015阅读 480评论 0 0