Promise

期约之前

回调地狱

设想这样一个经常发生的场景,我们希望处理Ajax请求的结果,所以我们将处理请求结果的方法作为回调传入,需要将请求结果继续处理,这就导致我们陷入了回调地狱

doSomething(function(result) {	// doSomething的结果以回调调用的形式传入doSomethingElse
doSomethingElse(result, function(newResult) { // doSomethingElse的结果以回调调用的形式传入doThridThing
doThirdThing(newResult, function(finalResult) { // doThridThing的结果以回调调用的形式最终被console.log调用
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);

而Promise的链式调用特点可以将需要链式处理的结果串联起来

doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

快速入门

Promise就像现实生活中的约定一样

  • 我们交给朋友一个任务,朋友给你他们要去完成任务的保证或者叫期约,然后你就可以干别的了,朋友继续帮你完成任务
  • 你的朋友既可能完成那个保证或叫期约,中间也有可能出现错误导致任务的失败
  • 成功时你会想知道成功的结果,失败时你会想知道失败的原因
  • 成功了你会有成功后的行为比如说请朋友吃顿饭,失败了你会有失败的行为比如让朋友请你吃顿饭
  • 最后你会想有一个最终的行为,比如说记个笔记说这次任务完成的怎么样,无论成功和失败你都想做的东西

对应Promise的过程

  • 拜托朋友完成指定任务和朋友答应你去完成任务的过程对应Promise的创建,也就是new Promise
  • 朋友成功完成任务通知你成功的过程对应Promise中调用resolve(),失败则对应Promise中调用reject()
  • 而成功与失败的原因,对应调用resolve()reject()时传入的参数,这个参数会传入最终的成功处理器、失败处理器中
  • 任务成功或失败后你的行为对应Promise中的成功处理器与失败处理器,这是两个函数,里面可以放一些成功时候的操作和失败时候的操作
  • 最终的行为对应Promise中的最终处理器

对应代码,下面举了一个执行setTimeout的例子,这里的任务就是定时器任务

const promise = new Promise((resolve, reject) => {
/**
* 安排异步任务,通常调用一些运行环境的异步API,例如IO操作和Ajax操作
* 同时为异步任务绑定resolve()和reject(),当异步任务成功和失败时调用他们
**/
setTimeout(() => {
resolve({message: "Promise keeped"}); // 这里直接成功resolve
}, 10 * 1000);
}); // promise.then()中第一个参数为成功处理器,第二个参数是失败处理器
promise.then((result) => { // 处理器的第一个参数是对应resolve()和reject()中传入的
console.log(result.message); // 打印"Promise keeped"
}); promise.finally(() => { // 绑定最终处理器
console.log("3-day holiday has begun");
}); /** 最终控制台输出结果
* "Promise keeped"
* "3-day holiday has begun"
*/

看完上面的代码后还是没有理解完整Promise逻辑没关系,下面详细介绍Promise的细节

详细描述

Promise的三个状态

  • 待定(pending):初始状态,此时任务已被安排,也就是任务进行当中,还没得到结果
  • 兑现(fulfilled):任务成功完成,Promise就会变成这个状态
  • 拒绝(reject):任务失败

使用构造函数new Promise创建时Promise默认在待定状态,直到转换为兑现或拒绝状态,这个状态变化是单向且仅可变化一次。从待定状态到成功状态是通过调用resolve()实现的,转换到失败状态则是通过调用reject()实现的,这两个回调会通过构造函数中第一个函数参数传入。

下面是创建一个待定态并直接跳过安排任务步骤直接转换为兑现态的代码

let promise = new Promise((resolve, reject) => {
resolve();
});
console.log(promise); // Promise {<resolved>: {...}}

下面是创建一个待定态并直接跳过安排任务步骤直接转换为拒绝态的代码

let promise = new Promise((resolve, reject) => {
reject();
});
console.log(promise); // Promise {<reject>: {...}}和Uncaught (in promise)错误

了解了基本的Promise任务转换之后,让我们引入需要真正安排异步任务的操作,这才是Promise安排任务的真正用法(虽然这段代码用处依然不算大,但足够展示Promise用法了)

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 10 * 1000);
});
console.log(promise); // Promise {<Pending>: {...}}

在上方的代码中,我们安排了一个异步任务也就是setTimeout,一旦定时器结束调用了resolve(),promise将立刻变为resolved的状态。同样道理可以为异步操作绑定Promise的失败状态调用reject()来将Promise状态转换为rejected

在没有Promise以前,我们不能拿到定时器的执行状态,现在有了Promise的帮助,我们可以看到在setTimeout()的10秒定时没有完成之前,promise将会是pending的状态。

那么给朋友安排任务、朋友执行任务然后将结果通知给你这两个步骤都完成了,接下来就是朋友将成功的结果或失败的原因传递给你、你根据朋友传过来的成功结果或失败原理做对应处理的过程了

Promise构造函数的resolve()reject()传入参数就是朋友告诉你成功结果和失败原因的途径,这两个函数传入的参数,会对应传递给成功处理器和失败处理器

Promise的状态转换会触发处理器函数的执行,对应你对朋友处理成功或失败的结果做出相应的反应

在继续这两个过程之前,我们再宏观的看一下Promise的三个重要使用过程

Promise的四个重要的过程

  • 安排异步任务
  • 成功、失败的调用绑定
  • 成功、失败、最终处理器的绑定
  • 成功、失败、最终处理器执行

上述四个过程中前三个需要我们进行编码,最后一个过程在异步事件完成后被触发。构造过程在上节详细描述过了,构造过程是安排异步任务、为异步任务绑定Promise的成功、失败转换调用(也就是resolvereject)的过程。

成功、失败调用的绑定,是创建完Promise之后的过程,用于绑定成功处理器与失败处理器给Promise,在异步操作执行完之后,成功或失败处理器将被调用做得出结果之后的处理,这个绑定操作可以通过Promise.prototype.thenPromise.prototype.catch来实现。

Promise.prototype.then接受两个参数,一个是成功处理器,一个是可选的失败处理器,成功、失败处理器各自可以接受一个参数,这个参数将是异步操作完成回调resolve()reject()传入的参数。

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ message: "Promise Resolved"});
}, 10 * 1000);
}); promise.then((value) => {
console.log(value.message); // "Promise Resolved"
});

Promise.prototype.catch只接受一个失败处理方法作为参数,这个失败处理器将会被传入reject()传入的值

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject({ message: "Promise Rejected"});
}, 10 * 1000);
}); promise.catch((value) => {
console.log(value.message); // "Promise Rejected"
});

所以四个过程的任务

  • 安排异步任务:在Promise构造函数中创建一个期约并安排期约的异步任务给朋友(异步API),你则持有这个期约用于之后绑定成功/失败处理器
  • 成功、失败调用的绑定:在Promise构造函数中给朋友(异步API)绑定上成功时的resolve()和失败时的reject()用于异步操作执行完成后转换期约的状态、调用成功/失败处理器、传入异步操作处理完成的数据
  • 成功、失败、最终处理器的绑定:使用你持有的期约上的then()catch()finally()来绑定成功、失败、最终处理器,成功、失败处理器会在Promise进入对应状态时被调用
  • 成功、失败、最终处理器的调用:promise状态在异步操作结束后调用resolve()reject()转换为fullfilledrejected状态从而触发已经绑定的成功处理器或失败处理器,无论达到以上两个之中的哪个状态,最终处理器都会被调用

成功、失败、最终处理器的绑定有两种方式,单独绑定和链式绑定,为promise绑定则处理器,处理器会按顺序执行;而使用链式绑定,每个then()catch()finally()都会返回Promise,即可以实现链式调用

let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("some value");
}, 1000)
}); promise.then(value => {
console.log(value); // "some value"
}).then(value => {
console.log(value); // undefined
return "other value";
}).then(value => {
console.log(value); // "other value"
});

then()catch()finally()中默认返回的Promise是开头绑定的Promise,但如果在处理器中显式返回了值则将返回被Promise.resolve()包裹的该值,也就是向下传递的Promise被换成了上一个处理器返回的内容Promise(上方代码第二、三个处理器展示了这种情况)。

链式处理时在处理器中抛出错误则会返回一个Promise.reject()传递给下方的处理器,若此异常未被后面绑定的catch()处理异常将在控制台抛出。

组合Promise

Promise异步操作也可以组合执行,ECMAScript提供了Promise.all()Promise.race()方法实现Promise的组合执行。组合执行Promise有许多场景,比如我们需要多个互不依赖的异步请求之后将所有请求结果汇总与拼合,我们就可以用到Promise.all();而Promise.race()为我们提供了一个拿到多个异步操作最快的那一个操作返回值的能力。

Promise.all()用于将多个用于安排异步任务的Promise并行执行,返回带有所有任务执行结果数组的Promise,当其中一个Promise出错则立即创建并返回一个带有这个错误信息的reject状态的Promise

let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("First Promise");
}, 1000)
}); let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Second Promise");
}, 2000)
}); Promise.all([promise1, promise2]).then(values => {
for (const value of values) {
console.log(value); // 大约2s之后打印: First Promise Second Promise
}
});

将上方的Promise.all()改写为Promise.race()将只会返回最早执行完成的异步任务的值

// ....
Promise.race([promise1, promise2]).then(value => {
console.log(value); // 大约1s之后打印: First Promise
});

async / await

async/await关键字是ECMA 2017(ES7)引入的Promise语法糖,它让Promise任务的调用变得简洁,让异步代码看起来不那么难理解。当JavaScript引擎处理执行到async修饰的方法,该方法会被推迟到其他同步代码执行完毕之后执行。

下面是一个例子,简单看一下例子之后会进一步解析async/await

// 下方方法返回一个安排了异步http请求的Promise,返回的Promise带有请求返回值
function request(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get(url, res => {
res.on('data', chunk => {
resolve(chunk.toString());
});
})
}, 1000); // 为了让效果明显一点
});
} // 上节学到的请求
function getInfo() {
request("http://localhost:3000/info").then(result => {
global.info = result;
});
// 没有办法直接在方法内获取promise执行结果
} // 使用async await
async function getUser() {
// 可以直接拿到promise执行结果
let result = await request("http://localhost:3000/user");
global.user = result;
return result;
} getUser().then(result => {
console.log(result);
});

async/await的本质是将原本在处理器内部编写的代码以同步的写法与Promise组合起来。在上方例子的使用async await注释下方代码中,我们以同步的方式执行了request异步任务,并将返回值绑定在全局变量user上,接下来我们返回了result并使用.then绑定了成功处理器,处理器中打印了result的值。

其实上方描述中async/await中执行的绑定代码效果,与给在request异步任务后绑定一个执行同样操作的处理器效果一致,也就是上方代码其实也可以这样实现

request("http://localhost:3000/user").then(result => {
global.user = result;
console.log(result);
});

那async/await的好处在哪,这种写法的好处就是当你需要链式调用Promise(需要区别于Promise的链式调用功能)时,这种写法会十分的整洁。

// 省略上方request的代码
request("http://localhost:3000/info").then(result => {
global.info = result;
return request("http://localhost:3000/base");
}).then(result => {
global.base = result; // 这里的result是请求/base获得的
return request("http://localhost:3000/user");
}).then(result => {
global.user = result;
}); // 上方代码可以简写为
async function getVariables() {
global.info = await request("http://localhost:3000/info");
global.base = await request("http://localhost:3000/base");
global.user = await request("http://localhost:3000/user");
} getVariables();

aynsc/await关键词不仅可以应用在普通方法上,还可以用在其他方式声明的方法上

// 一般方法声明
async function foo() { } // 匿名函数
let bar = async function() { } // 箭头函数
let baz = async () => { }
setTimeout(async () => {
global.res = await fetch("resource.json");
}, 1000); // 类成员函数
class Quz {
async qux() { }
}

由于async/await函数体内代码同步执行,错误处理使用try/catch正常捕获就可以。

总结

  • Promise是一个期约,它不是异步操作本体,而是一个方便你进行同步操作(安排异步任务、绑定成功失败处理器)的一个中介
  • Promise有三个状态,创建与异步操作未完成时状态为pending、操作完成状态转换为resolve、操作失败状态转换为rejected,后两个状态的Promise可以分别通过Promise.resolve()Promise.reject()直接创建
  • Promise的使用过程有四步,前两步为同步操作,最后一步为异步操作
    1. 安排异步任务并建立期约,(new Promise((resolve, reject)=>{ })并在箭头函数中为异步任务绑定成功时的回调(resolve)和失败时的回调(reject),Promise的状态转换通过异步任务回调这两个函数来实现
    2. 绑定处理器,为上一步建立的期约Promise绑定成功、失败处理器,这一步通过.then().catch()实现
    3. 异步任务执行,被浏览器或Nodejs环境自动执行。异步任务比如setTimeout、http请求等
    4. 异步任务执行完毕执行处理器,异步任务执行完毕浏览器或Nodejs环境会执行对应的第一步绑定的resolve()reject()转换Promise的状态,在Promise状态转换后环境会自动将对应处理器安排到微任务队列等待Event Loop安排执行
  • 一个Promise可以绑定多个处理器,处理器按照绑定顺序执行,默认为所有处理器传入resolve()回调传入的值。处理器也可以返回值,返回值会传入下一个处理器
  • 如果在链上任意位置报错,则可以使用绑定的失败处理器进行处理,若不处理错误将抛给控制台导致程序运行问题
  • 使用Promise.all()一次性为多个Promise绑定处理器,所有异步操作传入resolve()的值将组合为数组传递给成功处理器,如果有任意异步操作调用了reject()则传入的单个失败信息将会传给失败处理器。Promise.race()则提供了一个谁异步操作执行的最快处理器拿到谁的结果的方法
  • async/await是ES7推出的Promise语法糖,用于将Promise处理器内代码以同步的方式写在方法中让代码看起来更简洁

Promise 期约的更多相关文章

  1. JavaScript中的Promise【期约】[未完成]

    JavaScript中的Promise[期约] 期约主要有两大用途 首先是抽象地表示一个异步操作.期约的状态代表期约是否完成. 比如,假设期约要向服务器发送一个 HTTP 请求.请求返回 200~29 ...

  2. JavaScript高级程序设计(第4版)pdf 电子书

    JavaScript高级程序设计(第4版)pdf 电子书 免责声明:JavaScript高级程序设计(第4版)pdf 电子书下载 高清收集于网络,请勿商用,仅供个人学习使用,请尊重版权,购买正版书籍. ...

  3. JavaScript高级程序设计(第4版)知识点总结

    介绍 JavaScript高级程序设计 第四版,在第三版的基础上添加了ES6相关的内容.如let.const关键字,Fetch API.工作者线程.模块.Promise 等.适合具有一定编程经验的 W ...

  4. JavaScript中的异步函数

    JavaScript中的异步函数 ES8 的 async/await 旨在解决利用异步结构组织代码的问题.为此, ECMAScript 对函数进行了扩展,为其增加了两个新关键字: async 和 aw ...

  5. js--迭代器总结

    前言 我们已经熟练使用set.map.array几种集合类型了,掌握了map(),for..of..,filter()等迭代集合的方法,你是否思考过,js引擎是怎么迭代的,怎么判断迭代是否结束,本文来 ...

  6. Javascript高级程序设计(000)

    该分类下为学习Javascript高级程序设计的笔记,希望自己可以坚持学习,努力学习!加油! 一.组织结构 第 1 章,介绍 JavaScript 的起源:从哪里来,如何发展,以及现今的状况.这一章会 ...

  7. opencv模块学习

    一.简介 ''' 分辨率(resolution,港台称之为解析度)就是屏幕图像的精密度,是指显示器所能显示的像素的多少.由于屏幕上的点.线和面都是由像素组成的,显示器可显示的像素越多,画面就越精细,同 ...

  8. Timer定时器开发

    Timer定时器开发 定时器的作用是不占线程的等待一个确定时间,同样通过callback来通知定时器到期. 参考:https://github.com/sogou/workflow 定时器的创建 同样 ...

  9. JavaScript高级程序设计(第4版)-第一章学习

    第一章 什么是Javascript 一.历史 JavaScript的名字怎么来的 首先,我们从javascript的历史开始了解,在以前的时候网页要验证某个必填字段是否填写,或者是判断输入的值的正确与 ...

  10. vue 将markdown字符串转html、修改主题、生成目录

    前言 将 markdown 字符串转成 html 显示出来,同时把目录也提取出来一起显示.可以使用 marked 来读取 markdown 字符串解析成 html marked官网:https://m ...

随机推荐

  1. 全面升级!揭秘阿里云智能Logo设计的AI黑科技

    简介: 免费体验!阿里云智能logo设计一直致力于用AI技术,帮助更多有设计需求的朋友能"多快好省"地做logo,让"设计logo"这件有门槛的事情,通过智能工 ...

  2. Nacos2.0的K8s服务发现生态应用及规划

    ​简介:Nacos 是阿里巴巴于 2018 年开源的注册中心及配置中心产品,帮助用户的分布式微服务应用进行服务发现和配置管理功能.随着 Nacos2.0 版本的发布,在性能和扩展性上取得较大突破后,社 ...

  3. [FAQ] Git远程仓库想把目录大写改为小写,windows本地不识别的的处理

      通过四步操作: 1. 先把忽略大小写设为false,即区分大小写git config core.ignorecase false 2. 拷贝出来备份那几个大写的目录,随后分支上操作删除,提交到远程 ...

  4. dotnet 6 使用 Obfuscar 进行代码混淆

    本文来安利大家 Obfuscar 这个好用的基于 MIT 协议开源的混淆工具.这是一个非常老牌的混淆工具,从 2014 年就对外分发,如今已有累计 495.5K 的 nuget 下载量.而且此工具也在 ...

  5. 8.k8s之调动pod到指定节点与创建多容器pod并查找pod日志

    官方文档:将pod分配给节点题目1:调度pod到指定节点 设置配置环境kubectl config use-context k8s 按如下要求创建并调度一个pod: - 名称:nginx-kusc00 ...

  6. OLAP系列之分析型数据库clickhouse单机版部署(一)

    一.概述 官网:https://clickhouse.com/docs/zh ClickHouse是Yandex于2016年开源的列式存储数据库(DBMS),主要用于在线分析处理查询(OLAP),能够 ...

  7. SpringBoot使用JSch操作Linux

    推荐使用Hutool的Jsch工具包(它用的连接池的技术) 一.SSH远程连接服务器 SSH更多见:http://t.csdnimg.cn/PrsNv 推荐连接工具:FinalShell.Xshell ...

  8. AI实战 | 手把手带你打造智能待办助手

    背景 大家好,我是努力的小雨.今天我想分享一下搭建待办助手的经历.起初,我并没有什么特别的创意点子.但在4月16日的百度Create大会上,我看到了小度的大模型加持使得其变得更加智能.我被一场示例所震 ...

  9. GitLab 管理 NuGet 包

    1 概览 在服务器上构建项目时,需要引用 nuget.org 之外的包,如公司内部开发的.第三方未发布到 nuget.org 上的.怎么办? GitLab 提供了 Package Registry 来 ...

  10. three.js教程4-Group层级模型

    1.组对象Group.层级模型-形成树状结构 //创建两个网格模型mesh1.mesh2 const geometry = new THREE.BoxGeometry(20, 20, 20); con ...