Promise 是JS中一种处理异步操作的机制,在现在的前端代码中使用频率很高。Promise 这个词可能有点眼生,但你肯定见过 axios.get(...).then(res => {...});用于异步请求的 axios 返回的就是一个 Promise 对象。

平时一直在代码中 .then() .catch() 地写来写去,终于决定要认真学一学这个 Promise 到底是怎么回事,希望这篇学习笔记也能帮到你。

Promise 对象

一个 Promise 对象表示一个异步操作的执行结果,包括状态(成功/失败)和值(成功返回值/错误原因)。一个 Promise 在创建的时候异步操作可能还没执行完成,通过持有这个 Promise 对象,可以在未来异步操作完成的时候对结果进行相应操作。

Promise 对象的状态

这里有几个常用的名词,用来表示 Promise 对象当前的具体状态:

Pending 待定:刚创建时的初始状态,还没有确定执行结果

Fulfilled 已兑现:异步操作执行成功,并返回一个值

Rejected 已拒绝:异步操作执行失败,并返回一个错误原因

Settled 已敲定 / Resolved 已决议:“待定”状态的反面,都表示异步操作已经执行完成,即已兑现已拒绝

回调函数

如果完全不关心异步操作的执行结果,那就把它放在那自己执行就可以了;但通常情况下我们总是要对操作执行的结果进行后续处理的,例如更改页面上的数据显示、错误处理等。但由于异步操作不知道什么时候可以执行完成,就出现了“回调函数”的概念,意思就是等到异步操作处理结束了,再回过头来调用这个函数来对执行结果进行处理。

传统做法是,在执行异步操作的时候就把回调函数作为参数传进去,比如最常见的:

setTimeout(function(){
console.log("成功!");
}, 250);

setTimeout() 函数是最常见的异步函数之一,众所周知它的作用就是在指定时间后执行指定代码。仔细看就会发现,setTimeout() 函数接收两个参数,第二个参数是等待时间,而第一个参数就是回调函数,即等待指定的时间之后要回来调用这个函数。

很显然这种传参的做法有很多不方便的地方,比如把对结果的后续处理和异步操作本身耦合在了一起,以及著名的回调地狱:

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

Promise.then() 绑定回调函数

有了 Promise 之后,就能把回调和异步操作本身分开了。无论一个 Promise 对象当前是否已经执行完毕,我们都能在它上面绑定回调函数,并且保证回调函数被执行;这就是喜闻乐见的 then() 方法。

p.then(onFulfilled[, onRejected]);
p.then(value => {
// fulfillment
}, reason => {
// rejection
});

then() 方法的语法很简单,有两个可选参数,分别代表当 Promise 的状态变为成功(fulfilled)和失败(rejected)时所使用的回调函数。

如果只想绑定 onRejected(即失败时的错误处理函数),下面两种写法完全等价,第二种是第一种的简写形式。

p.then(null, failureCallback);
p.catch(failureCallback);

使用 Promise:链式调用

如果只是用 then 来绑定回调函数,那并不能解决回调地狱的问题。然而很妙的地方来了: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);

链式调用的实现

能做到链式调用的魔法来自 then() 方法:它会在执行相应的回调函数之后,返回一个新的 Promise 对象,并且插入 Promise 链的当前位置

这里稍微有点绕,容易把回调函数等同于 then() 方法本身。实际上成功/失败的回调函数只是 then() 的参数而已;而实际执行 then() 的时候,它会先根据 promise 的状态调用相应的回调函数,再根据回调函数的执行结果生成一个新的 Promise 对象并返回;具体的对应规则如下:

回调函数执行情况 then() 返回的 Promise 对象
返回值 return x; fulfilled 状态,参数为 x
直接返回 return; / 无 return 语句 fulfilled 状态,参数为 undefined
抛出错误 throw err; rejected 状态,参数为 err
返回已决议的 Promise 状态和参数与返回的 Promise 一致
返回未定的 Promise 未定的 Promise,回调参数与返回的相同

下面这个例子中,初始 Promise 的状态为已拒绝,然后第一个 then() 调用了绑定的 onRejected,返回了状态为 fulfilled 的新 Promise 对象,并传递给了链中的下一个 then():

Promise.reject()
.then(() => 99, () => 42) // 调用 onRejected(return 42;),表格中的第一种情况
.then(solution => console.log('Resolved with ' + solution)); // Resolved with 42

同时,你可能还记得 then() 的参数定义,两个回调函数都是可选的;如果没有传入对应的回调函数,then() 会直接把原 promise 的终态返回,不做额外处理。

错误处理

遇到异常抛出(被拒绝的 promise)时,浏览器会顺着 Promise 链寻找下一个 onRejected 回调函数(经常被简写为 .catch()),并跳过中间的 onFulfilled 回调函数。这种执行逻辑与同步代码中的 try-catch 执行过程非常相似:

// 异步 Promise
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback); // 同步
try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}

一个具体的例子:

Promise.resolve()
.then(() => {
throw new Error('出错');
console.log('a');
})
.then(() => {
console.log('b');
})
.catch(() => {
console.log('c');
})
.then(() => {
console.log('d');
}); // 输出结果:
// "c"
// "d"

常见错误

doSomething().then(function(result) {
doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// 最后,是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常

上面这个例子在 Promise 中进行了嵌套,但没有将嵌套的 Promise 对象返回,因此doFourthThing() 不会等待 doSomethingElse()doThirdThing() 完成,而是并行运行;并且如果有传入参数,接收到的会是 undefined 而不是 doThirdThing() 的执行结果。

正确的写法应该是:

注:箭头函数 () => x() => { return x; } 的简写,即返回了新的 Promise 对象

doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

创建 Promise 对象

如果要执行的异步操作没有返回 Promise 对象,可以用 new 和构造器创建自己的 promise。构造器的两个参数的作用是在异步操作成功/失败时,转换 Promise 对象的状态并传递对应参数。

const myFirstPromise = new Promise((resolve, reject) => {
// 做一些异步操作,最终会调用下面两者之一:
// resolve(someValue); // fulfilled
// reject("failure reason"); // rejected
}); // 一个例子
function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
};

Promise 其他静态方法

创建已决议的 Promise 对象

Promise.resolve(value)Promise.reject(reason) 方法分别返回一个已经成功/失败的 Promise 对象。

const p1 = new Promise((resolve, reject) => {
resolve();
});
const p2 = Promise.resolve();

如果 resolve(value) 的参数是带有 then() 方法的 Promise 对象,函数将返回其自带 then() 方法的执行结果;如果参数为空或是基本类型,返回的Promise对象与在 then() 方法中 return 对应值的结果一致,参见上文表格。基于这样的特性, resolve(value) 方法可以用于将不确定是否为 Promise 对象的 value 值统一为 Promise。

多个 Promise 对象

Promise.all(iterable)

  • 参数列表中的所有的 promises 都成功时,返回一个 fulfilled 的 Promise 对象,参数值是所有 promises 成功返回值的列表(顺序不变)
  • 如果任何一个 promise 失败,立即返回一个 rejected 的 Promise 对象,参数是这个失败 promise 的错误信息
Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

Promise.allSettled(iterable)

列表中所有 promises 都已敲定后返回一个promise,并带有一个对象数组,对应每个promise 的结果。

Promise.any(iterable)

当列表中的任意一个 promise 成功时,立即返回这个 promise 的值。

Promise.race(iterable)

当列表中任意一个 promise 成功或失败时,立即返回该 promise 对象的执行结果。

一个综合例子(使用 setTimeout 模拟异步操作):

// 创造一个状态为 fulfilled,参数为"foo"的 Promise 对象
Promise.resolve("foo")
.then(function(string) { // string: "foo"
// 返回状态为 fulfilled,参数为"foobar"的对象
return new Promise(function(resolve, reject) {
setTimeout(function() {
string += 'bar';
resolve(string);
}, 1);
});
})
.then(function(string) { // string: "foobar"
setTimeout(function() {
string += 'baz';
console.log(string);
}, 1)
// 返回值"foobar"
return string;
})
.then(function(string) { // string: "foobar"
console.log("Last Then");
console.log(string);
}); // 输出结果:
// Last Then
// foobar
// foobarbaz(由第二个 then 中的 setTimeout 输出)

结语&参考文献

以上是阅读学习了 MDN 文档后个人总结的学习笔记,可能存在错误和疏漏,欢迎指正与讨论!

MDN-Promise

MDN-使用Promise

MDN-Promise.then()

前端 | JS Promise:axios 请求结果后面的 .then() 是什么意思?的更多相关文章

  1. 前端 | JS 任务和微任务:promise 的回调和 setTimeout 的回调到底谁先执行?

    首先提一个小问题:运行下面这段 JS 代码后控制台的输出是什么? console.log("script start"); setTimeout(function () { con ...

  2. vue 路由拦截、axios请求拦截

    路由拦截 项目中,有些页面需要登录后才能进入,例如,在某页面A,用户在操作前需要先进入登录页(此时需要将上一页的地址(/survey/start)作为query存入login页面的地址中,如: htt ...

  3. vue 解决axios请求出现前端跨域问题

    vue 解决axios请求出现前端跨域问题 最近在写纯前端的vue项目的时候,碰到了axios请求本机的资源的时候,出现了访问报404的问题.这就让我很难受.查询了资料原来是跨域的问题. 在正常开发中 ...

  4. 前端Vue项目——首页/课程页面开发及Axios请求

    一.首页轮播图 1.elementUI走马灯 elementUI中 Carousel 走马灯,可以在有限空间内,循环播放同一类型的图片.文字等内容. 这里使用指示器样式,可以将指示器的显示位置设置在容 ...

  5. 前端快闪四: 拦截axios请求和响应

    马甲哥继续在同程艺龙写一点大前端: 今天我们来了解一下 如何拦截axios请求/响应? axios是一个基于 promise 的网络请求库,可以用于浏览器和 node.js, promise 类似于C ...

  6. 过滤器会拦截 前端页面加载 js文件的请求

    学艺不精啊.....之前就总结过博客: JAVA中解决Filter过滤掉css,js,图片文件等问题 结果现在又犯了老错误~ 情况如下: index.jsp 页面的验证码输入栏绑定了异步验证(jQur ...

  7. 解决axios请求本地的json文件在打包后路径出错问题

    vue 项目中使用axios请求了本地项目的static文件夹下的json文件,使用npm run build 打包后,在Hbuilder编辑器打开,页面报错404: 在浏览器打开的路径 http:/ ...

  8. 解决跨域问题,前端 live-server --port=1802 后端启动 localhost:1801,以及解决 vue 的 axios 请求整合

    测试的源码文件内容点击跳转 前端引入 vue.js 与 axios.min.js <script src="https://cdn.bootcss.com/vue/2.6.10/vue ...

  9. vue-ajax/axios请求函数封装: axios+promise

    项目文件目录/src/api ajax.js /** * ajax 请求函数模块 * 返回值为promise对象 */ import axios from 'axios' export default ...

随机推荐

  1. 神奇的数学学习网站 All In One

    神奇的数学学习网站 All In One magical math websites {{uploading-image-923797.png(uploading...)}} Math is Fun ...

  2. PIP & Python packages management

    PIP & Python packages management $ python3 --version # OR $ python3 -V # Python 3.7.3 $ pip --ve ...

  3. record terminal sessions

    record terminal sessions asciinema https://asciinema.org/ # install $ brew install asciinema # Start ...

  4. Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析

    目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...

  5. 前端监控SDK开发分享

    目录 前言 收集哪些数据 性能 错误 辅助信息 小结 客户端SDK(探针)相关原理和API Web 微信小程序 编写测试用例 单元测试 流程测试 提供Web环境的方式 Mock Web API的方式 ...

  6. Java的稀疏数组的简单代码实现

    目录 Java的稀疏数组的简单代码实现 一.稀疏数组的基本概念 二.稀疏数组的Java代码实现思路 三.稀释数组的Java代码实现 四.结语 Java的稀疏数组的简单代码实现 一.稀疏数组的基本概念 ...

  7. jenkins+docker+nginx+tomcat实现vue项目部署

    一.项目准备 1.新建一个vue的项目,确保能在浏览器正常访问.然后在项目的根目录下新建一个Dockerfile的文件,内容如下 FROM nginx COPY dist /usr/share/ngi ...

  8. Element-UI使用相关问题

    1.如何修改el-dialog的样式? 要修改dialog的样式不能直接在<style scoped>中修改,这样修改后不会生效.做法是把scoped去掉,然后在dialog标签上自定义一 ...

  9. SpringBoot注解集合

    使用注解的优势: 1.采用纯java代码,不在需要配置繁杂的xml文件 2.在配置中也可享受面向对象带来的好处 3.减少复杂配置文件的同时亦能享受到springIoC容器提供的功能 @SpringBo ...

  10. iOS中web与Js的交互

    问题 感觉到uni-app框架有pit,公司强推该框架的小哥识趣的闭嘴,考虑到全盘替换周期跟成本挺大,基于uni-app能打包成H5,采用webview+js的原生方式集成 基本结构:原生壳 + we ...