肥宅钓鱼网
当前位置: 首页 钓鱼百科

promise 现场版(一首歌带你搞懂Promise)

时间:2023-08-19 作者: 小编 阅读量: 1 栏目名: 钓鱼百科

Promise简介Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。,但是对于Promise的创建和执行流程,其实有很多人初学时,容易走入误区。今天就以一首歌来带大家学习Promise。Promise创建Promise翻译过来的意思就是"承诺",其实Promise的创建本质就是一个许下承诺的过程,想到这我的脑海中不由得浮现了一首经典老歌,海鸣威《你的承诺》。

Promise简介

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。(摘自<ECMAScript 6 入门>),但是对于Promise的创建和执行流程,其实有很多人初学时,容易走入误区。

今天就以一首歌来带大家学习Promise。

Promise创建

Promise翻译过来的意思就是"承诺",其实Promise的创建本质就是一个许下承诺的过程,想到这我的脑海中不由得浮现了一首经典老歌,海鸣威 《你的承诺》。

歌中相知相恋的两个人,奈何情深缘浅,终于还是分开,许下了“不为彼此难过, 过各自的生活"这样的承诺。但是随后歌曲又以"Oh baby 你答应我的我都记得 ,但是你却忘了你的承诺 ,不是说好彼此都不再联络, 谁都别再犯错",暗示承诺的状态发生改变。

由此不难想象以下两种情况:

  1. 遵守承诺
  1. 违背承诺

同样,JS中的Promise的创建也是许下代码承诺的一种方式,它默认也包含三种状态:

  1. pending (进行中)
  2. fulfilled(已成功)
  3. rejected(已失败)

那么如何许下JS中的承诺呢:

var p = new Promise(function(){ // 回调函数})

Promise中接收一个回调函数(简单说就是一个容器),传入某个未来才会结束的事件(通常是一个异步操作)的结果;此时打印,你会得到一个Promise实例(实例化对象),展开之后默认显示:

Promise {<pending>} [[Prototype]]: Promise, [[PromiseState]]: "pending", [[PromiseResult]]: undefined,

此时我们会看到

[[PromiseState]]用于存储Promise实例(当前承诺)的状态 ,默认显示pending ;[[PromiseResult]]用于表示Promise实例中存储的数据 , 默认显示undefined;[[Prototype]]表示原型 以后再做讲解

但是有人就会问了,既然有三种状态,而默认是pending(进行中)状态,那么我们如何将当前Promise实例的状态改为另外两种:

这就不得不提到Promise的第一特征了:

对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

可以理解为,状态并不是自行改变的,而是由异步(同步也可以)的结果决定了.。

所以Promise在创建过程中,传入的回调函数接收了改变状态的两个方法 ,resolve,reject作为形式参数传入.

var p = new Promise(function(resolve,reject){//形式参数 // 回调函数})

resolve() 将Promise实例的状态 由pending(进行中) 改为 fulfilled(已成功)

reject() 将Promise实例的状态 由pending(进行中) 改为rejected(已失败)

而且resolve() reject() 方法在调用时 还可以接收一个参数,作为数据传入,存储到Promise实例中。

同步代码展示:

var p = new Promise(function(resolve,reject){//形式参数 // 回调函数 resolve(111)})console.log(p)结果:Promise {<fulfilled>: 111}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"// 已成功[[PromiseResult]]: 111// 存储数据var p = new Promise(function (resolve, reject) {//形式参数// 回调函数reject(222)})Promise {<rejected>: 111} [[Prototype]]: Promise [[PromiseState]]: "rejected"// 已失败 [[PromiseResult]]: 222// 存储数据Uncaught (in promise) 111// 报错 (可以不用理会后面会被then() / catch() 方法捕获)

resolve() 和 reject() 方法,在同步代码中调用,会立即改变Promise实例的状态。

异步代码展示:

var p = new Promise(function (resolve, reject) {//形式参数setTimeout(function () {resolve(111);console.log("line 24:", p);}, 2000)})console.log("line 27:", p);结果依次为:line 27: Promise {<pending>}line 24: Promise {<fulfilled>: 111}-------------------------------------两次代码请分开执行-------------------------------------------------var p = new Promise(function (resolve, reject) {//形式参数setTimeout(function () {reject(111);console.log("line 24:", p);}, 2000)})console.log("line 27:", p);结果依次为:line 27: Promise {<pending>}...2s后line 24: Promise {<rejected>: 111}报错: 2.html:60 Uncaught (in promise) 111(可以不用理会后面会被then() / catch() 方法捕获)

resolv e() 和 reject() 方法,在异步代码中调用:由于JS是单线程,会优先在主线程执行同步代码,异步代码会放到任务队列中,所以在页面加载时,,Promise实例为pending(进行中)状态,等待主线程执行完毕,任务队列通知主线程,异步任务可以执行了,该任务才会进入主线程执行。

此时setTimeout中的回调函数被调用,执行了resolve() 或 reject() 方法, Promise实例会随之改变状态,并存储传入的数据。

那么resolve() reject() 方法,可以重复调用么?

这就要讲到Promise的第二大特征:一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型) 注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

由此可见Promise本质,就是一个状态机,当该Promise对象创建出来之后,其状态就是pending(进行中),然后通过程序来控制到底是执行已完成,还是执行已失败。

因为Promise处理的是异步任务,所以我们还得对Promise做监听,当Promise的状态发生变化时,我们要执行相应的函数(then catch finally)。

Promise的动态方法

所谓Promise动态方法,即将封装的方法存储在Promise的原型对象(prototype)上,供所有通过构造函数创建的实例化对象使用。

Promise.prototype.then()

Promise 实例具有then方法,它的作用是为 Promise 实例添加状态改变时的回调函数,前面说过 Promise实例的状态可以从pending(进行中) 变为 fulfilled(已成功)或rejected(已失败),所以then方法中接收两个回调函数(它们都是可选的)。

p.then(resolveHandler,rejectHandler)

resolveHandler 第一参数,用于指定Promise实例,由pending(进行中) 变为 fulfilled(已成功)时执行的回调函数。

rejectHandler 第二参数,用于指定Promise实例,由pending(进行中) 变为 rejected(已失败)时执行的回调函数。

可以理解为 then()方法中的两个回调函数,提前规定好了状态改变时需要执行的内容,等待以后Promise实例的状态之后再执行对应的回调函数 (本质还是回调函数,要用魔法打败魔法)。

注意:resolveHandler rejectHandler 中可以传入一个形式参数 接收Promise实例中存储数据。

// 等待两秒后 随机一个0-1的数,如果 num>=0.5 就变为fulfilled,否则变为rejectedvar p = new Promise(function (resolve, reject) {//形式参数setTimeout(function () {var num = Math.random();if (num >= 0.5) {resolve(111)} else {reject(2222);}console.log("line 24:", p);}, 2000)})console.log("line 27:", p);p.then(function (arg) { console.log("line 50", arg);}, function (arg) { console.log("line 52", arg);})情况1: (num>=0.5)line 27: Promise {<pending>}...2秒后line 24: Promise {<fulfilled>: 111}line 50 111情况2: (num<0.5)line 27: Promise {<pending>} ...2秒后2.html:81 line 24: Promise {<rejected>: 2222}2.html:89 line 52 2222

解析:在异步代码中调用: 由于JS是单线程,会优先在主线程执行同步代码,异步代码会放到任务队列中,所以

  1. Promise创建完毕,Promise实例为pending(进行中)状态;
  2. 调用 then()方法,传入两个回调函数,提前规定好了状态改变时需要执行的内容,等待以后Promise实例的状态之后再执行对应的回调函数;
  3. 等待主线程执行完毕,任务队列通知主线程,异步任务可以执行了,该任务才会进入主线程执行。=> 随机一个0-1的随机数,根据结果改变Promise实例的状态 ,存储传入的数据 => 执行then方法中对应的回调函数。

还有一个有趣的地方:then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

var p = new Promise(function (resolve, reject) {//形式参数setTimeout(function () {resolve(1);}, 2000);})p.then(function (num) {console.log("line 64:", num);return 2;}).then(function (num) {console.log("line 67:", num);return 2;})结果:line 64: 1line 67: 2

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。当然这里有一个前提,那就是Promise实例的状态为已成功且代码执行过程中不出错的情况下。万一出错的话,那我们就可以使用then()方法的第二个回调函数或者catch()方法捕获错误。

Promise.prototype.catch()

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

function readText(url) {var p = new Promise(function (resolve, reject) {$.ajax({type: "get",url: url,success: function (text) {resolve(text);},error:function(err){reject(err);}})})return p; // {pending}}readText("../data/1.txt").then(res => {console.log("line 93", res)}).catch(err => {console.log(err);})

上面代码中,readText()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

then和catch的链式操作

then()和catch()配合使用,实现的链式操作效果更佳。

var p = new Promise(function (resolve, reject) {setTimeout(function () {resolve(1);}, 2000);})console.log("line 21", p);p.then(function (num) {console.log("line 23:", num);return 2;}).then(function (num) {console.log("line 25:", num);}).then(function (num) {console.log("line 28:", num);}).catch(function (err) {console.log(err);})结果:line 21 Promise {<pending>}...等待2s后line 23: 1line 25: 2line 28: 3

上面的代码使用then catch方法进行链式调用,依次指定了三个then()的回调函数以及一个catch()的回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数,同样第二个回调函数完成后,会将返回结果作为参数,传入第三个回调函数,以此类推。当然这里同样也有一个前提,那就是Promise实例的状态为已成功且代码执行过程中不出错的情况下。万一出错的话,那catch()方法刚好捕获链式操作过程中出现的错误。

如果then方法中回调函数的返回值也是一个Promise实例;

function readText(url) {var p = new Promise(function (resolve, reject) {$.ajax({type: "get",url: url,success: function (text) {resolve(text);},error: function (err) {reject(err);}})})return p; // {pending}}// ajax恐怖回调的解决方式1:// T = T1T2T3(时间)var str = "";readText("../data/1.txt").then(txt => {console.log("line 36", txt);str= txt;return readText("../data/2.txt");}).then(txt => {console.log("line 40", txt);str= txt;return readText("../data/3.txt");}).then(txt => {console.log("line 44", txt);str= txt;return str;}).then(txt => {console.log("line 48", txt);}).catch(err => {console.log("失败:", err);})结果:line 36 11111line 40 22222 line 44 33333 line 48 111112222233333

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,继续链式调用后面then方法中指定的回调函数,如果状态变为rejected,就会被catch方法捕获。

注意:Promise 实例的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

Promise的静态方法

所谓Promise静态方法,即将封装的方法存储在构造函数Promise上,由构造函数自身调用。

Promise.all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

相较于then方法的链式操作,Promise.all()更注重并发,即依次按顺序发送多个请求,等待所有的请求均有结果之后,对应请求顺序将数据依次存放到数组中返回。

模拟封装(简易版)Promise.myAll = function (list) {return new Promise(function (resolve, reject) {// 返回一个新的Promise实例var arr = [];for (let i = 0; i < list.length; i) {let p = list[i];// 每一个Promise实例(如果传入的数据非Promise实例 会通过Promise.resolve转化)// 提前规定好每一个实例 成功和失败时执行的内容=> 请求有结果之后会执行p.then(res => {//等待异步操作有结果之后 对应下标放到数组中arr[i] = res;if (arr.length === list.length) { // 所有的请求全都成功 => (也是一个异步操作)resolve(arr);}}).catch(err => {//只要有一个失败 就走catchconsole.log(err);reject(err);})}}) }const p = Promise.myAll([p1, p2, p3]);

通过以上代码可以发现: (摘自ECMAScript 6 入门)

Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race

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

const p = Promise.race([p1, p2, p3]);

相较于Promise.all方法, Promise.race方法更侧重状态改变的速度。

模拟封装(简易版) Promise.myRace = function (list) {return new Promise(function (resolve, reject) {// 返回一个新的Promise实例for (let i = 0; i < list.length; i) {let p = list[i];// 每一个Promise实例 (如果传入的数据非Promise实例 会通过Promise.resolve转化)// 提前规定好每一个实例 成功和失败时执行的内容=> 请求有结果之后会执行p.then(res => {//等待异步操作有结果之后 对应下标放到数组中resolve(res);}).catch(err => {reject(err);})}})}const p = Promise.myRace([p1, p2, p3]);

通过以上代码可以发现: (摘自ECMAScript 6 入门)

只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.allSettled

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

const p = Promise.allSettled([p1, p2, p3]);

相较于Promise.all方法,Promise.allSettled跟注重于不管每一个Promise实例是成功还是失败,都会将结果依次按请求顺序放到数组中。

模拟封装(简易版) Promise.myAllSettled = function (list) {return new Promise(function (resolve, reject) {// 返回一个新的Promise实例var arr = [];for (let i = 0; i < list.length; i) {let p = list[i];// 每一个Promise实例(如果传入的数据非Promise实例 会通过Promise.resolve转化)// 提前规定好每一个实例 成功和失败时执行的内容=> 请求有结果之后会执行p.then(res => {//等待异步操作有结果之后 对应下标放到数组中 var obj = { status: "fulfilled", value: txt }; arr[i] = obj; if (arr.length === list.length) { // 请求全都成功 => arr(异步)resolve(arr); }}).catch(err => { var obj = { status: "rejected", reason: err }; arr[i] = obj; if (arr.length === list.length) { // 请求全都成功 => arr(异步)resolve(arr); }})}})}const p =Promise.myAllSettled([p1, p2, p3])

值得注意的地方时,在将数据放到数组的过程中,传入的Promise实例的状态不同,放置的结果稍有不同(数组中的每个成员是一个对象,对象的格式是固定的)。

// 异步操作成功时{status: 'fulfilled', value: value}// 异步操作失败时{status: 'rejected', reason: reason}

成员对象的status属性的值只可能是字符串fulfilled或字符串rejected,用来区分异步操作是成功还是失败。如果是成功(fulfilled),对象会有value属性,如果是失败(rejected),会有reason属性,对应两种状态时前面异步操作的返回值。

总结

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

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

升级用法 async await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

基本用法

async function fn() {var str = "";var text = await readText("../data/1.txt"); // Promise实例console.log("line 29:",text);str= text;var text = await readText("../data/2.txt"); // Promise实例console.log("line 33:",text);str= text;var text = await readText("../data/3.txt"); // Promise实例console.log("line 37:",text);str= text;return str;}fn().then(res=>{console.log("line 44:",res)}).catch(err=>{console.log("line 46:",err)})结果:line 29: 11111 line 33: 22222 line 37: 33333 line 44: 111112222233333

async函数的返回值也是一个Promise实例,在async函数执行的过程中,一旦遇到await就会先返回pending(进行中)状态的Promise实例,等待异步操作有结果之后,继续执行await之后的语句,依次类推,语句全部执行完毕且无错误的情况下,则返回的Promise实例会变为已成功,否则会变为已失败。

你学会了么?文章底部留言告诉我喲。

    推荐阅读
  • 范冰冰海报被曝(范冰冰近照曝光)

    12月6日,有网友爆料,自己在美国某超市遇到了范冰冰和她妈妈一起闲逛超市。可能是因为怕冷,范冰冰身穿长款的银色加厚羽绒服,披散着头发,看起来心情不错。值得注意的是,范冰冰和妈妈购买了不少水果蔬菜,看起来瘦身又健康。当天,范冰冰从国内飞往美国,在机场被粉丝围绕。言外之意即范冰冰这次不是怀孕,只是又吃胖了。而且范冰冰现在似乎并不在意外界的流言蜚语。

  • 一国两制的发言汇总(罗援一国两制)

    “一国”涉及主权,主权问题不容讨论。这对大陆方面来讲轻车熟路,经过艰辛曲折的探索,社会主义制度已经在中国大陆取得成功实践,举世公认。众所周知,“一国两制”方案是小平同志对人类社会国家体制的大胆探索和创新,是大陆方面向台湾同胞释放的最大善意和诚意,是将台湾同胞利益的最大化。

  • 廊坊文安县异地定点医院名单 廊坊市异地就医定点医院

    廊坊文安县有以下异地定点医疗机构:医疗机构名称机构分类联系地址文安县妇幼保健院医院县城东关文安浙华医院医院文安县文安镇兴文东道文安古城医院医院文安县城东大街9号文安东方医院医院文安县政通道广场西街36号文安安定医院医院文安县南关外文王北路西侧文安城区医院医院文安县丰利路199号文安崇文医院医院文安城内南环路和西环路交口处东行100米路南文安泰康医院医院文安县左各庄镇皇道口南100米文安苏子会医院医

  • 春天穿衣搭配方法大全(春季穿衣要懂得分场合)

    春天暂时告别沉闷的黑色西装,选择轻盈淡雅的浅色系西装会更应景。春日出游,别忘了牛仔外套和宽松舒适的圆领卫衣。“小香风”粗花呢外套,绝对是经典的单品,优雅又很有名媛淑女的气质。黑色的小香风外套,容易显老气,化解的方法是搭配一条色彩温柔清新的印花连衣裙,加上复古风的单鞋,优雅又时髦。也可以选择米白色的小香风短裙套装,端庄优雅又不失活泼灵动,精致又贵气。方法就是加入休闲甜美的单品。

  • 冬青果储存方法(冬青果怎么储存)

    冬青果储存方法冬青科冬青属常绿小乔木或灌木,喜温暖湿润和阳光充足的环境,但也耐阴,有一定的耐寒性。盆栽宜用含腐殖质丰富、肥沃、且排水良好的微酸性土壤,一般在春季2-3月萌动前上盆,栽后浇透水,放在半荫处缓苗2-3周。夏季高温时要加强通风,稍作遮荫,防止烈日曝晒。冬季移至阳光充足的低温室内,保持盆土偏干一些,0℃以上即可安全越冬。

  • 明镜高悬什么意思(明镜高悬指什么)

    接下来我们就一起去研究一下吧!明镜高悬什么意思明镜高悬,是汉语成语,拼音是míngjìnggāoxuán,意思是传说秦始皇有一面镜子,能照人心胆;比喻官员判案公正廉明。出自《西京杂记》卷三:“有方镜,广四尺,高五尺九寸,表里有明,人直来照之,影则倒见。以手扪心而来,则见肠胃五脏,历然无碍。”

  • 打电话显示呼叫失败 给对方打电话显示呼叫失败

    手机、为移动电话或无线电话,通常称为手机,原本只是一种通讯工具,早期又有大哥大的俗称,是可以在较广范围内使用的便携式电话终端,最早是由美国贝尔实验室在1940年制造的战地移动电话机发展而来。1958年,苏联工程师列年,美国摩托罗拉工程师马丁库帕发明了世界上第一部商业化手机。历经2G时代、3G时代,迄今为止已发展至4G时代了,而5G时代也紧随其后,国内已经出现5G的商用。

  • 绝地求生在线人数回暖(绝地求生真凉了)

    最近,吃鸡在线人数的不败神话被DOTA2打破了。即使今年五月份平均玩家人数还在87.6万,但近一个月,《绝地求生》的在线人数已只剩下43.5万,并且仍然有下滑的趋势。最近,《绝地求生:刺激战场》就推出了“恶意伤害队友即时惩罚措施”。最近,在腾讯的协助下,平阳警方成功破获一起特大《绝地求生》外挂案件。捣毁了一个外挂窝点,共抓捕34名犯罪嫌疑人。掐指一算,2017年11月22号,腾讯正式宣布代理吃鸡国服。

  • 胶水怎么做起泡胶(起泡胶的做法)

    胶水怎么做起泡胶在杯中倒入少半杯胶水再加入少量的纯甘油,挤入沐浴露,沐浴露可以给起泡胶增加超强拉丝效果。用筷子将杯子里的胶水、甘油、沐浴露搅拌至混合均匀。在器具中倒入胶水和少量甘油。如果起泡胶较硬,可以额外加一点水进去。在搅拌的时候,根据实际情况加入水和胶水即可调整起泡胶的软硬。如果起泡胶比较干燥,最好是加入一些甘油,这样就能保证之后玩的时候比较舒适。

  • 粉肠怎么做又嫩又好吃(粉肠如何做又嫩又好吃)

    下面内容希望能帮助到你,我们来一起看看吧!粉肠怎么做又嫩又好吃材料:粉肠1副,葱2根,姜1块,盐适量,米酒1/2杯。将准备好的粉肠用清水洗净,然后再将葱切段、姜切片留着备用。另取锅放入作法2的粉肠、葱、姜及适量水,煮开后加入盐、米酒,再以小火煮约50分,闷20分即可取出切段盛盘。