Promise 完全解读

目录

  • 一. Promise 简介
    • Promise 是什么?
    • 我们为什么需要 Promise?
    • Promise 能解决什么?
    • Promise 的特点
    • Promise 的缺点
  • 二. Promise API 解析
    • Promise 的构造方法
    • Promise 的实例方法
    • Promise 类级别方法
  • 三. Promise 深度剖析
    • Promise 与 setTimeout
    • 多层级 .then()

一. Promise 简介

1. Promise 是什么?

ES6 出现的目标是为了使 JavaScript 语言可以编写大型的复杂应用程序,使之成为企业级的开发语言,Promise 也是其中的一环。

从语法上讲 Promise 是个内置对象,抽象来看,它像是一个容器,里面保存着一个异步操作的执行结果,Promise 提供了一套 API 以保证所有异步操作的统一处理方法。

2. 我们为什么需要 Promise?

我们先来举一个简单的 “栗子”,某人需要做三件事(A,B,C),并且要按照这个顺序依次执行,现在将这个转换为传统的代码方式:

// 首先我们要先定义这三件事(A,B,C)分别为三个函数
// 这三个函数都需要提供一个回调来表示事情的结束
function A(callback) {
  console.log('Do A.');
  callback();
}

function B(callback) {
  console.log('Do B.');
  callback();
}

function C(callback) {
  console.log('Do C.');
  callback();
}

// 现在,如果我想要依次做这三件事我需要这样。。。
A(function() {
  // 其它的一些代码
  B(function() {
    // 其它的一些代码
    C(function() {
      // 其它的一些代码
    });
  });
});

可以看出,当涉及到异步操作时,曾经的大部分方式都是靠回调的嵌套来实现的,然而这样的方式造成了几个十分严重的问题:

  • 出现了多层回调,当加上业务代码后,将会使整体显得臃肿且凌乱。
  • 回调的出现导致逻辑的流程不清晰,不具有可读性和维护性。
  • 当嵌套层级过多时会产生大量无用数据的滞留以及数据的混杂。
  • 使用回调函数便完全浪费了 return 和 throw 关键字的能力。

综上所述,我们需要一种更好的解决办法在某些(并不是全部)方面来取代回调(callback)方式的异步操作。

3. Promise 能解决什么?

  • 编写大型应用时,一种高级、实用且能解决实际问题的高大上语法。
  • 避免层层嵌套的回调函数,将异步操作以同步操作的流程表达出来,使逻辑更加清晰。
  • Promise 提供统一的接口来进行异步操作。

4. Promise 的特点

Promise 是基于状态的,一个 Promise 对象表示了一个异步操作,而这个操作会有三种状态:Pending(进行中)、Resolved(已成功)和 Rejected(已失败)。只有异步操作的执行结果可以决定是哪一种状态,任何其它操作都无法改变。而且,一旦状态改变,就不会再变化。

Promise 的异步操作不需要像回调一样执行异步操作后立即就会调用回调,Promise 允许任何时候都可以得到这个异步操作的结果。也就是说它其实和事件的机制是完全不同的,事件触发时,如果你没有处于监听状态,那么错过了就再也得不到此次事件的结果,而 Promise 则是将结果状态会凝固,等待你去观察这个异步操作的结果。

5. Promise 的缺点

一旦构造了 Promise 实例就代表执行了一个异步操作,也就是说它会立即执行,并且中途无法取消。

如果不设置回调,Promise 内部抛出的错误,不会反应到外部。(有利有弊,具体看怎么用)

Promise 其实只适合单一的异步程序,并不适合不断发生的事件处理,所以使用时,要找好最适合使用的场景。

二. Promise API 解析

1. Promise 构造方法

Promise 本身就是个构造函数,用来生成 Promise 实例。

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

resolve 函数的作用是将 Promise 对象的状态从 Pending 变为 Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。

reject 函数的作用是将 Promise 对象的状态从 Pending 变为 Rejected,在异步操作失败时调用,并将异步操作失败所抛出的错误,作为参数传递出去。

const promise = new Promise((resolve, reject) => {
  // 模拟一个异步操作
  setTimeout(function() {
    if ( /* 异步操作成功 */ ) {
      // 异步操作成功,携带载荷将状态变为 resolved
      resolve(payload);
    } else {
      // 异步操作失败,携带错误将状态变为 rejected
      reject(error);
    }
  }, 1000);
});

Promise 实例生成后,异步操作就已经开始执行了。

要注意,resolve 和 reject 的调用是对 Promise 对象状态的变更和数据的传递,并不会影响函数的执行,所以 resolve 和 reject 后面如果有可以正常执行的流程代码,它们仍然会被正常执行。如果不想这样,可以使用 return 强制函数执行的结束。

2. Promise 实例方法

(1) Promise.prototype.then()

Promise 实例生成以后,可以用 then 方法来指定 Resolved 状态和 Rejected 状态的回调函数

promise.then((payload) => {
  // Resolved 时执行
}, (error) => {
  // Rejected 时执行
});

Promise.prototype.then() 的两个参数都需要传递函数,代表着两个状态转变所要执行的回调,每个回调都可以接受 Promise 对象状态转变时传出的值作为参数。其中,then() 的第二个函数是可选的。

回到一开始我们举的 “栗子”,现在,我想通过 Promise 的方式来实现多个异步程序的依次执行,我们可以在 then() 的调用中显式的返回一个新的 Promise 实例,然后就可以链式的且依次的执行 then(),看下面的代码:

const promise = new Promise((resolve, reject) => {
  // 模拟异步程序 1:睡觉 1s
  setTimeout(() => {
    resolve('睡完觉了');
  }, 1000);
});

promise.then((val) => {
  console.log(val);
  return (new Promise((resolve, reject) => {
    // 模拟异步程序 2:吃饭 1s
    setTimeout(() => {
      resolve('吃完饭了');
    }, 1000);
  }));
}).then((val) => {
  console.log(val);
  return (new Promise((resolve, reject) => {
    // 模拟异步程序 3:喝水 1s
    setTimeout(() => {
      resolve('喝完水了');
    }, 1000);
  }));
}).then((val) => {
  console.log(val);
});

通过在 then() 的第一个回调函数中,返回新的 Promise 实例,我们可以用同步的流程将异步的操作表示出,相比使用 callback 更加的逻辑清晰。

(2) Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数,也可以捕获 then() 运行中所抛出的错误。

一般来说,好的方式是不再 then() 里面指定 Rejected 的回调,而是使用 catch() 来对所有错误的捕获(包括 then() 里面抛出的错误)

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

promise.then(() => {
  return temp + 2;
}).catch((error) => {
  console.error(error);
});

// ReferenceError: temp is not defined

3. Promise 类级别方法

(1)Promise.all()

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

Promise.all() 接受一个类数组(具有 Iterator)作为参数,每个成员应为 Promise 实例,如果不是,会调用 Promise.resolve() 方法将其转换为 Promise 实例。

Promise.all() 返回的新的 Promise 实例的状态由传递的类数组成员的共同状态决定:

  • 当所有成员的状态都变为 Resolved,Promise 实例的状态才会变为 Resolved,此时所有成员的返回值组成一个数组,传递给 Promise 实例的回调函数
  • 只要有一个成员的状态变为 Rejected,Promise 实例的状态就变成 Rejected,此时第一个被 Rejected 的实例的返回值会传递给 Promise 实例的回调函数

如果作为 Promise.all() 参数的 Promise 实例自己定义了 catch 方法,那么它的状态变为 Rejected 时,只会触发它自己的 catch(),不会触发 Promise.all() 的 catch()

(2)Promise.race()

Promise.race() 方法和 Promise.all() 几乎是一样的,只是对状态的处理存在差别。

Promise.race() 返回的新的 Promise 实例的状态由传递的类数组成员中最先改变状态的成员的状态决定。

(3)Promise.resolve()

Promise.resolve() 将现有对象转换为 Promise 对象。

Promise.resolve() 的参数分成四种情况:

  • Promise 实例:直接返回这个实例。
  • thenable 对象
    • thenable 对象指的是具有 then 方法的对象。
    • Promise.resolve() 会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法,相当于将这个对象的 then 方法作为 Promise 构造函数的参数,返回一个新的 Promise 实例。
  • 其余情况
    • Promise.resolve() 返回一个新的 Promise 对象,状态为 Resolved,使用 then() 时,会将 Promise.resolve() 的参数传递给 then() 的回调函数中。
    • 这个立即 Resolved 的 Promise 对象,实在本次 “事件循环” 的结束时才开始执行。

(4)Promise.reject()

Promise.reject() 会返回一个新的 Promise 实例,状态为 Rejected。

Promise.reject() 等价于下面的写法:

Promise.reject(obj);
// 等价于
new Promise((resolve, reject) => reject(obj));

Promise.reject() 会将参数原封不动的作为 reject() 的参数。

三. Promise 深度剖析

1. Promise 与 setTimeout

我们来看这样的一段代码:

setTimeout(() => {
  console.log(1);
}, 0);

(new Promise((resolve, reject) => {
  resolve();
})).then(() => {
  console.log(2);
});

console.log(3);

这段代码的运行结果的顺序是 3,2,1,原因如下:

  • 对于 setTimeout 我们应该都知道,它是放在下一轮 “事件循环” 的开始,所以它一定要在本轮事件结束后才会执行,也就是输出 1 一定要在输出 3 以后
  • 接下来就是 Promise 异步执行的问题,虽然 Promise 实例中并没有真正意义上的异步程序,而是直接将状态变更为 Resolved,且立即使用 then 进行状态的观察,但是实质上,立即 Resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务,所以,输出 2 一定要在输出 3 以后
  • 最后,就是 Promise 和 setTimeout 之间的问题,上面也说到了,setTimeout 是在下一轮事件的开始,而 Promise 又实在本一轮事件的结束,所以,很明显,输出 1 要在输出 2 以后

2. 多层级 .then()

一个 Promise 实例可以连续使用 .then() 来绑定回调函数:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

promise.then(() => {
  console.log(1);
}).then(() => {
  console.log(2);
});

// 1
// 2

如果在 .then() 的回调中显式的指定一个返回值(非 Promise 实例),这个值会被作为下一个链式 .then() 回调函数中的参数:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

promise.then(() => {
  console.log(1);
  return 'Promise';
}).then((val) => {
  console.log(val);
});

// 1
// Promise

如果在 .then() 的回调中返回的是一个 Promise 实例,那么下一个链式 .then() 会在这个 Promise 实例的状态变更时会被调用,并且 resolve 所传递的值会被作为下一个链式 .then() 回调函数中的参数:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

promise.then(() => {
  console.log(1);
  return (new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve('Promise');
    }, 1000);
  }));
}).then((val) => {
  console.log(val);
});

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

推荐阅读更多精彩内容

  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 2,132评论 0 12
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,357评论 0 19
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 2,170评论 0 16
  • nhhiuftyiy
    zf1阅读 229评论 0 0
  • 95年的大水瓶 喜欢吃零食爱唱歌 虽然唱的不怎么样。爱跑步喜欢自拍有点臭美哈。爱逛街喜欢旅游。所以互相认识一下咯 ...
    偶然的邂逅阅读 105评论 0 0