JavaScript中的Promise详解

一、什么是异步

异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。总所周知,JavaScript 的代码执行的时候是跑在单线程上的,代码按照出现的顺序,从上到下一行一行的执行,也就是我们说的同步(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。

简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

实际开发中,某些业务的结果我们是不能同步获取的,而等待的结果也是不确定的。比如异步操作 ajax 获取数据,遍历一个大型的数组(同步操作),还有动态加载脚本文件然后初始化相关业务。

function loadScript(src) {
  let script = document.createElement("script");
  script.src = src;
  document.head.append(script);
}
loadScript("./js/script.js");
init(); // 定义在 ./js/script.js 里的函数

这样的代码肯定是达不到效果的。因为加载脚本是需要花时间的,是一个异步的行为,浏览器执行 JavaScript 的时候并不会等到脚本加载完成的时候再去调用 init 函数。

远古时代的做法是使用回调函数,给处理函数传递一个回调函数来处理回调结果。

function loadScript(src, success, fail) {
  let script = document.createElement("script");
  script.src = src;
  script.onload = success;
  script.onerror = fail;
  document.head.append(script);
}
loadScript("./js/script.js", success, fail);
function success() {
  console.log("success");
  init(); // 定义在 ./js/script.js 中的函数
}
function fail() {
  console.log("fail");
}

试想一下,如果 success 函数又需要异步处理回调呢?OMG,那简直就是灾难,传说中的"回调地狱"青面獠牙向我们走来,它是恶魔,即使是聪明绝顶的程序猿都避而远之。

为了避免"回调地狱"吞噬程序猿茂密的毛发,Promise解决方案就应运而生了。

二、Promise

Promise,是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观,人称“护发金刚”。顾名思义为承诺、许诺的意思,意思是使用了 Promise 之后他肯定会给我们答复,无论成功或者失败都会给我们一个答复。

我们可以把异步操作交给 Promise 来处理,什么时候处理好他通知我们,如果还有异步操作再交给 Promise 处理,这样就可以将异步的操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

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

纵使她有这些缺点,但丝毫影响我们对他的欢喜,甚至有些猿友对她爱得死去活来的。

Promise 首先是一个对象,它通常用于描述现在开始执行,一段时间后才能获得结果的行为(异步行为),内部保存了该异步行为的结果。然后,它还是一个有状态的对象:

  • pending:待定
  • fulfilled:兑现,有时候也叫解决(resolved)
  • rejected:拒绝

一个 Promise 只有这 3 种状态,且状态的转换过程有且仅有 2 种:

  • pending 到 fulfilled
  • pending 到 rejected

三个属性,两个技能,她就是"回调地狱"的克星,程序猿的知音啊。

创建 Promise

调用 Promise 构造函数来创建一个 Promise。

let promise = new Promise((resolve, reject) => {});

Promise 构造函数接收一个函数作为参数,该函数的两个参数是 resolve,reject,它们由 JavaScript 引擎提供。

其中 resolve 函数的作用是当 Promise 对象转移到成功,调用 resolve 并将操作结果作为其参数传递出去;reject 函数的作用是当 Promise 对象的状态变为失败时,将操作报出的错误作为其参数传递出去。

由 new Promise 构造器返回的 Promise 对象具有如下内部属性:

  • PromiseState:最初是 pending,resolve 被调用的时候变为 fulfilled,或者 reject 被调用时会变为 rejected。
  • PromiseResult:最初是 undefined,resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error。

三、Promise 常用方法

Promise.prototype.then()

调用 then 可以为实例注册两种状态的回调函数,当实例的状态为 fulfilled,会触发第一个函数执行,当实例的状态为 rejected,则触发第二个函数执行。

  • onResolved:状态由 pending 转换成 fulfilled 时执行。
  • onRejected:状态由 pending 转换成 rejected 时执行。
function onResolved(res) {
  console.log("resolved" + res); // resolved3
}
function onRejected(err) {
  console.log("rejected" + err);
}
new Promise((resolve, reject) => {
  resolve(5);
}).then(onResolved, onRejected);

Promise.prototype.catch()

catch 只接受一个参数,也就是 rejected 抛出的值,一般用于异常处理。传统的try/catch捕获不了Promise内部的异常的,因为抛出异常这个动作是异步的。

在处理异常的时候,我们可以在catch中进行异常的捕获,也可以直接抛出异常。

function onRejected(err) {}
new Promise((resolve, reject) => {
  reject();
}).catch(onRejected);

Promise.prototype.finally()

finally()方法只有当状态变化的时候才会执行,可以用来做一些程序的收尾工作,比如操作文件的时候关闭文件流。

function onFinally() {
  console.log(12345); // 并不会执行
}
new Promise((resolve, reject) => {}).finally(onFinally);

all()

Promise 的 all 方法提供了并行执行异步操作的能力,在 all 中所有异步操作结束后才执行回调。

function p1() {
  var promise1 = new Promise(function (resolve, reject) {
    console.log("p1的第一条输出语句");
    resolve("p1完成");
  });
  return promise1;
}

function p2() {
  var promise2 = new Promise(function (resolve, reject) {
    console.log("p2的第一条输出语句");
    setTimeout(() => {
      console.log("p2的第二条输出语句");
      resolve("p2完成");
    }, 2000);
  });
  return promise2;
}

function p3() {
  var promise3 = new Promise(function (resolve, reject) {
    console.log("p3的第一条输出语句");
    resolve("p3完成");
  });
  return promise3;
}

Promise.all([p1(), p2(), p3()]).then(function (data) {
  console.log(data);
});

输出结果:

p1的第一条输出语句;
p2的第一条输出语句;
p3的第一条输出语句;
p2的第二条输出语句[("p1完成", "p2完成", "p3完成")];

race()

在all中的回调函数中,等到所有的Promise都执行完,再来执行回调函数,race则不同它等到第一个Promise改变状态就开始执行回调函数。将上面的all改为race

function p1(){
  var promise1 = new Promise(function(resolve,reject){
      console.log("p1的第一条输出语句");
      resolve("p1完成");
  })
  return promise1;
}

function p2(){
  var promise2 = new Promise(function(resolve,reject){
      console.log("p2的第一条输出语句");
      setTimeout(() => {
        resolve("p2完成");
      }, 2000);
  })
  return promise2;
}

function p3(){
  var promise3 = new Promise(function(resolve,reject){
      console.log("p3的第一条输出语句");
      resolve("p3完成")
  });
  return  promise3;
}

Promise.race([p1(),p2(),p3()]).then(function(data){
  console.log(data);  
})

结果:

p1的第一条输出语句
p2的第一条输出语句
p3的第一条输出语句
p1完成

p1完成返回后就不等p2,p3的返回了。

四、练习题

1、下面的代码输出什么?

new Promise((resolve, reject) => {
  console.log('A')
  resolve(3)
  console.log('B')
}).then(res => {
  console.log('C')
})
console.log('D')
// 打印结果:A B D C

上面这串代码的输出顺序是:A B D C。

解答

我们知道,立即执行函数会在 new Promise 调用的时候同步执行。所以为先打印出AB,然后遇到resolve(3)onResolved函数会被推入微任务队列,然后就打印D,此时所有同步任务执行完成,浏览器会去检查微任务队列,发现存在一个,所以最后会去调用 onResolved 函数,打印出 C。

注意,执行 resolve()/reject()/finally()时都是异步的。

2、下面的代码输出什么?

new Promise((resolve, reject) => {
    resolve(1)
}).then(res => {
    console.log('A')
}).finally(() => {
    console.log('B')
})
new Promise((resolve, reject) => {
    resolve(2)
}).then(res => {
    console.log('C')
}).finally(() => {
    console.log('D')
})
// 打印结果:A C B D

上面这串代码的输出顺序是:A C B D。

解答

  • 执行 resolve(1),将处理程序 A 推入微任务队列 1;
  • 执行 resolve(2),将处理程序 C 推入微任务队列 2;
    同步任务执行完成,执行微任务队列 1 里的内容,打印 A,A 所在函数执行完成后生成了一个 fulfilled 的新实例,由于新实例状态变化,所以会立即执行 finally() 处理程序 B 推入微任务队列 3;
  • 执行微任务队列 2 的内容,打印 C,C 所在函数执行完成后,同上条原理会将处理程序 D 推入微任务队列 4;
  • 执行微任务队列 3 的内容,打印 B;
  • 执行微任务队列 4 的内容,打印 D;
  • 代码全部执行完成,最终打印:A C B D。

最后

欢迎关注公众号【前端技术驿站】,回复5678获取最新前端实战视频

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

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,686评论 0 5
  • 作者: kim先生 来源: 自创 今天我们讲的是ES6中的Promise这个异步操作对象。在学习Promise之前...
    前端进阶体验阅读 732评论 0 0
  • 前言 若是对执行队列,宏任务,微任务的不太理解的,建议先阅读 这一次,彻底弄懂 JavaScript 执行机制(别...
    lessonSam阅读 748评论 0 1
  • 前面的话 JS有很多强大的功能,其中一个是它可以轻松地搞定异步编程。作为一门为Web而生的语言,它从一开始就需要能...
    CodeMT阅读 601评论 0 0
  • 以下题目是根据网上多份面经收集而来的,题目相同意味着被问的频率比较高(x3表示有三份面经被问),有问题欢迎留言讨论...
    Aniugel阅读 2,331评论 0 7