es6入门4--promise详解
可以说每个前端开发者都无法避免解决异步问题,尤其是当处理了某个异步调用A后,又要紧接着处理其它逻辑,而最直观的做法就是通过回调函数(当然事件派发也可以)处理,比如:
请求A(function (请求响应A){
//请求响应A作为参数调用方法B
funB(请求响应A);
});
但从业务角度来说,回调往往不会只有一层;例如我项目中有一个购物车结算的需求:我需要先给网站A下个单,然后以A请求返回的单号为参数调用另一个借口,以给网站B下一个回执单,回执单拿到之后才是跳转页面,大概是这样:
下单A(function (请求响应A){
//下单A响应成功后调用下单B
下单B(function(请求响应B){
//下单B成功后跳转
window.location.href = '我是订单页'
});
});
如果请求再多点呢,通过回调的做法我们只能层层嵌套,这也就诞生了让代码维护者头痛的回调地狱。想想几个月后,你的同事或者自己重新阅读这段代码时,函数嵌套与未分离的大量业务代码,不头大都难!
而Promise的出现正好解决了这一痛点,通过promise我们能以链式调用的形式取代传统回调嵌套的写法,同时还能将逻辑代码从毁掉地狱中抽离出来,改写上面上面的例子,像这样是不是好看了很多:
new Promise(下单A)
.then(下单B(单号A))
.then(页面跳转)
一、基本用法
Promise强大的地方就在于能通过对异步请求状态的改变,与我们达成一种承诺;例如将请求的状态由pending(进行中)改为fulfilled(已完成),我们就可以通过then方法对应的回调处理相关后续逻辑了。
Promise对象的状态一共有三种,进行中pending,已成功fulfilled(resolved)与已失败rejected。值得一提的是,promise对象的状态不会受外界影响,但我们可以通过内置方法对状态进行改变。且状态一旦改变,此状态就会定型;直白点说就是假设我们将pending改为resolved后,不管什么时候再去访问它,状态将永远维持为已完成状态。我们通过一个例子证实这一点:
let fn = (resolve, reject) => {
//改为resolved
resolve(1);
//再次修改状态无效
reject(2);
};
let p = new Promise(fn);
//输出1
p.then(resp => console.log(resp), err => console.log(err));
1.创建promise实例并通过then回调
Promise对象是一个构造函数,我们可以通过new来新建一个Promise实例:
function promiseDemo(resolve, reject) {
if (true) {
//异步请求成功
resolve(1);
} else {
reject(respError);
};
};
let promise = new Promise(promiseDemo);
console.log(promise);
在new的操作中,我们需要给Promise传递一个函数作为参数,且此函数中可直接使用resolve与reject参数,用于修改异步请求的状态。
在上述代码中,我们假设发起了一次异步请求,同时拟定true为异步请求成功,通过resolve处理请求成功的返回数据,得到了一个promise实例。通过打印可以看到状态被修改为已成功(resolved),同时得到了响应成功数据value为1;
现在尝试使用then处理回调,可以看到成功输出了1,而这个1是我们拟定异步请求成功返回的数据。
promise.then(function (resp){
//异步请求成功后续逻辑
console.log(resp);//
},function (resprror){
//请求失败逻辑
});
那上面就是一个模拟的简单的promise例子了,下面具体聊聊promise内置方法,看看promise具体有哪些用法。
二、Promise方法介绍
1.Promise.resolve()
Promise.resolve()能将一个对象转为promise对象。
我在前面说,通过new Promise能得到一个promise实例对象,其实通过Promise.resolve()也能创建一个promise实例,下面这个例子可以看到得到的结果是相同的:
let promise1 = Promise.resolve(1);
let promise2 = new Promise(resolve => resolve(1));
console.log(promise1, promise2);
需要注意的是Promise.resolve()接受的参数不同,处理参数的行为也将不同。
1.1传递一个promise对象作为参数
如果我们为Promise.resolve()传递一个promise对象,那么它将会将此对象原封不同的返回:
let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(promise1);
console.log(promise1 === promise2);//true 还是原来的味道
当我们使用promise作为参数用于创建另一个promise对象时,还需要注意promise具有状态传递的特性:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => reject(1), 3000)
}); const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(p1), 1000)
});
//等待四秒后触发reject回调,输出失败了
p2.then(resp => console.log('成功了'),err => console.log('失败了'));
上述代码中,p2的状态由p1决定,四秒后p1 then方法还是触发了失败回调。
1.2传递一个非对象,例如数字,字符串
当我们传递一个非对象作为参数,Promise.resolve会返回一个promise对象,状态为resolved,且通过then回调我们能正常访问该参数。
let promise1 = Promise.resolve(1);//第一步执行
console.log(promise1);//第二步执行
promise1.then(resp => console.log(resp));//then方法最后执行
console.log(2);//第三步执行
在ES6入门这本书中说,由于传递的参数不具备异步行为(不带有then方法),所以Promise.resove同步执行修改了参数状态,并立刻执行then回调;但事实并非如此;如上,通过断点将执行先后步骤加在了注释里,打印2的操作要早于then,then依旧是最后执行,这里特别指出。
我对于这里的理解是,不管是new Promise()中的resolve()还是Promise.resolve(),只要不具备异步行为,resolve方法本身都将同步执行,但是then回调仍然是异步触发。
一个new Promise例子:
let demo = resolve => {
resolve(1);
};
let promise = new Promise(demo);
//此时的promise状态已修改为resolved,说明resolve(1)已触发
console.log(promise)
promise.then(resp => {
console.log(resp);
});
console.log(2);
这个例子中依次打印promise,2,1。且打印出的promise对象状态为resolved,说明resolve()方法为同步执行,但then回调最后触发。
一个Promise.resolve()例子:
let promise = Promise.resolve(1);
console.log(promise);
promise.then(resp => console.log(resp))
console.log(2);
依旧是第一打印promise,第二次打印1,且promise状态是resolved,说明在打印1之前,Promise.resolve(1)已经执行完成,then回调最后触发执行。
这里我加个例子与上面的代码做对比,让resolve处理异步操作,下面的代码才符合resolve完成立刻触发then回调的情况:
let demo = resolve => {
console.log(1);
setTimeout(() => {
resolve(4);
}, 1000);
console.log(2);
};
let promise = new Promise(demo);
promise.then(resp => console.log(resp));
console.log(3);
上述代码依次会输出1,2,3,4,这个例子中resolve()方法由于异步的问题等到同步代码跑完了才触发了,同时resolve完成立刻触发了then方法,最后输出了4。
1.3.传递一个thenable对象
thenable对象是指带有then方法的对象,我们可以手动创建此类对象,我个人感觉angular中$http返回的对象应该也是thenable对象。
对于thenable对象resolve方法会将此对象转为promise对象,得到的实例也能正常通过then方法回调。
let thenable = {
then: (resolve, reject) => resolve(42)
};
let p1 = Promise.resolve(thenable);
p1.then(value => console.log(value));//
1.4.不传递参数
如果不传递参数,则得到一个没有value,但状态是resolved的promise对象。
let promise = Promise.resolve();
console.log(promise);
2.Promise.reject()
Promise.reject()也会返回一个promise实例,状态为rejected。reject接收的参数会原封不动作为reject回调时的参数,不像resolve那么多情况。
let thenable = {
then: function (resolve, reject) {
reject(1);
}
};
let p = Promise.reject(thenable);
p.then(resp => {
console.log(resp)
}, err => {
console.log(err === thenable)//true
});
3.Promise.prototype.then()
为什么次方法是Promise.prototype.then()而不是Promise.then()呢,这是因为then方法是为Promise实例提供,而实例的方法是通过继承而来,then方法在Promise对象的原型链上也就合情合理了。
通过前面的例子,我们也知道了then方法提供了2个回调函数,第一个对应resolved状态,第二个对应rejected状态。
需要注意的是,then方法会隐性返回一个新的promise实例,我们甚至可以无限使用then回调都不会报错:
let p = Promise.resolve(1);
p.then(resp => console.log(resp)) //
.then(resp => console.log(resp)) //undefined
.then(resp => console.log(resp)) //undefined
// ...无数个
也正因为这个特性,我们在处理异步请求A的then回调中,可以手动返回一个异步请求B的promise实例,通过这样的做法也就实现了同步链式的写法:
let p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
p1.then(resp => {
return p2;
}).then(resp => {
return p3;
}).then(resp => {
console.log(resp);//
});
4.Promise.prototype.catch()
总是推荐使用catch()方法代替then方法中的第二个回调;这是因为catch方法不仅能捕获异步请求的错误,它还能捕获then方法的错误,但then的第二个回调做不到这一点。
let p = Promise.resolve(1);
p.then(resp => {
console.log(x);
}).catch(err => {
console.log(err);//x is not defined
});
上述代码中我们在成功回调中故意打印一个未定义的变量x,catch成功帮我们捕获了这个错误,但是如果使用then第二个错误回调,是无法捕获的。
const fn = (resolve,reject) => {
console.log(x);
};
let p = new Promise(fn);
p.then(resp => {
console.log(1);
}).catch(err => {
console.log(err);//x is not defined
});
这个例子中,我们在创建promise实例的函数中故意出错,catch也捕获了错误,虽然这个错误then第二个回调也能做到,但整体来说catch更为强大,这也是推荐使用catch而不是then第二个回调的理由。
说到处理错误,Promise还有个奇怪的地方,假设Promise出错了,但没使用then第二回调或者catch处理错误;尽管程序会报错,但这个错误并不会抛出给外层,所以外层程序并不会因此停止执行,所以在then回调后面跟一个catch方法是有必要的。
const fn = (resolve, reject) => {
resolve(1);
};
let p = new Promise(fn);
p.then(resp => {
console.log(x);
});
setTimeout(() => {
console.log(1);
}, 0);
5.Promise.prototype.finally()
finally()方法有点像switch case中的default,不管你异步成功了还是失败了,finally都会如约而至的触发。一般用法是这样:
const p = Promise.resolve(1);
p.then(resp => console.log(resp))
.catch(err => console.log(err))
.finally(() => console.log('执行完毕'));
finally方法因为不关心Promise状态,所以不需要传递参数,在ES6入门中也提到,由于不管状态成功或者失败都会触发,所以finally也等同于then方法中使用两个回调的做法。
6.Promise.all()
Promise.all()方法接受多个promise实例,返回一个全新的promise实例:
let p = Promise.all([p1, p2, p3]);
如果p1,p2,p3不是promise实例,则会在all执行前先为这三个参数执行Promise.resolve()方法。
all方法返回的promise实例的状态由参数共同决定,以上面代码为例,如果三个promise实例状态全部为resolved,则p的状态便为resolved,但如果三个实例有一个未rejected,则p的状态便为rejected。
//全部为resolved
let p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
let p = Promise.all([p1, p2, p3]);
p.then(resp => console.log(resp));//[1,2,3]; //部分为reject
let p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.reject(3);
let p = Promise.all([p1, p2, p3]);
p.then(resp => console.log(resp))
.catch((err) => console.log(err));//2;
这里有个需要注意的地方,如果一个状态为rejected的promise实例使用了then方法的第二回调,或者使用了catch()方法,这会导致all()无法触发自己catch()方法或者then的第二回调。
let p = Promise.reject(1).then(resp => resp, err => err);
// 或者
// let p = Promise.reject(1).then(resp => resp)
// .catch(err => err);
Promise.all([p])
.then(resp => console.log('成功执行'))//成功执行
.catch(err => console.log('报错啦'));
上述例子中创建promise对象时虽然使用了reject方法,但由于自身有捕获错误的操作,导致实例p拿到的是then方法返回的另一个promise对象。我们可以看看状态:
let p = Promise.reject(1).then(resp => resp, err => err);
console.log(p);
不管什么时候都应该记住,then方法也会返回一个新的promise对象。
7.Promise.race()
Promise.race()同样是接受多个promise实例返回一个全新promise实例的方法,但与all方法不同的地方在于,决定这个promise状态的是多个实例参数中最先改变状态的那个。
let p = Promise.race([p1, p2, p3]);
假设p3最先改变状态成了rejected,那么p的状态也就是rejected。p1,p2随后再改变状态将不会对p实例起作用。
let p1 = Promise.reject(1);
let p2 = Promise.resolve(resole => {
setTimeout(() => {
resole(2);
}, 3000)
});
Promise.race([p1, p2])
.then(resp => console.log(resp))
.catch(err => console.log(err));//1
上述例子中p1是一个同步执行状态为rejected的promise实例,p2是异步创建状态为resolved的实例,由于p2需要等三秒,所以最终race的实例以p1为主,这里最终输出1。
那么到这里promise基本方法就介绍完了,一个个例子去理解以及自己钻牛角尖也花了一点时间,接下来应该会写下promise执行顺序的文章,这个与宏任务微任务挂钩,还有就是手写promise需要看下,那么这篇文章就写到这里。
如果对于JS执行机制相关有疑问,可以阅读博主这篇博文
欢迎大家留言讨论!!!
es6入门4--promise详解的更多相关文章
- ES6中的Promise详解
Promise 在 JavaScript 中很早就有各种的开源实现,ES6 将其纳入了官方标准,提供了原生 api 支持,使用更加便捷. 定义 Promise 是一个对象,它用来标识 JavaScri ...
- 经典Spring入门基础教程详解
经典Spring入门基础教程详解 https://pan.baidu.com/s/1c016cI#list/path=%2Fsharelink2319398594-201713320584085%2F ...
- Solr安装入门、查询详解
Solr安装入门:http://www.importnew.com/12607.html 查询详解:http://www.360doc.com/content/14/0306/18/203871_35 ...
- angular $q promise详解
前言 通过本文,你大概能清楚angular promise是个啥,$q又是个啥,以及怎么用它.这里咱们先灌输下promise的思想. 下面写的全是废话,一些看着高逼格其实没什么大作用的概念,想知道$q ...
- ES6入门之变量的解构赋值(二)
前言 在上一章 ES6入门之let和const命令中我们对ES6的相关语法已经有了初步了解,上一章中我们主要学习了三大部分的内容,let命令的使用,块级作用域,const命令的使用,那么从本篇博客将进 ...
- 音视频入门-11-PNG文件格式详解
* 音视频入门文章目录 * PNG 文件格式解析 PNG 图像格式文件由一个 8 字节的 PNG 文件署名域和 3 个以上的后续数据块(IHDR.IDAT.IEND)组成. PNG 文件包括 8 字节 ...
- 音视频入门-14-JPEG文件格式详解
* 音视频入门文章目录 * JPEG 文件格式解析 JPEG 文件使用的数据存储方式有多种.最常用的格式称为 JPEG 文件交换格式(JPEG File Interchange Format,JFIF ...
- 使用IDEA创建Maven项目和Maven使用入门(配图详解)
本文详解的讲解了使用IDEA创建Maven项目,及Maven的基础入门. 1.打开IDEA,右上角选择File->New->Project 2.如图中所示选择Maven(可按自己所需添加, ...
- WIN中SharePoint Server 2010 入门安装部署详解
目前流行的原始安装文件基本都是这样的:Windows Server 2008 R2+SQL Server 2008R2+SharePoint Server 2010 这个初始环境原本也无可厚非 ...
- 【IOS 开发】Object-C 入门 Xcode 环境详解
作者 : 韩曙亮 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/38424965 一. Xcode 环境安装 与 工程创建 1. ...
随机推荐
- temp--内蒙农信(环境)
规章制度篇: 1, 内蒙农信办公地址: 呼和浩特市赛罕区内蒙古自治区农村信用社联合社(陶利街) 农金大厦1201室. 2, 电子版蓝底照片(办饭卡,自己充钱) , 行里面吃饭标准 早餐8元.午 ...
- For语句的衍生对象
for in语句: for...in 语句用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作). for...in 语句用于对数组或者对象的属性进行循环操作. for ... in 循环中的 ...
- easyUI dialog打开对话框,显示列表数据,选取一条数据操作后赋值给父窗口 resultMap声明为全局,生成getset方法
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" ...
- CentOS 7 rpm安装jdk
RPM 安装jdk1.8.0_111 ,查询系统自带的jdk rpm -qa | grep java 查询结果如下: [root@bogon ~]# rpm -qa | grep java javap ...
- openXML设置Excel行组合
重要代码: row = new DOS.Row() { RowIndex = new DOX.UInt32Value(rowIndex), Spans = new DOX.ListValue<D ...
- grep,sed,awk用法整理
grep -c 打印出符合要求的行数 -i 忽略大小写 ignore -n 连同符号一起输出 num -v 打印出不符合要求的行 -A2 本行及下面两行 - ...
- Linux 区别 chown和chmod的用法
chown用法用来更改某个目录或文件的用户名和用户组的chown 用户名:组名 文件路径(可以是就对路径也可以是相对路径)例1:chown root:root /tmp/tmp1就是把tmp下的tmp ...
- Python time库常用函数
time模块中时间表现的格式主要有三种: timestamp 时间戳,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量 struct_time 时间元组,共有九个元素组. for ...
- Html/CSS前端如何实现文字边框阴影
上次我们聊了<Html5前端如何实现文字阴影>,其实在开发中现在对于阴影效果的使用已经越来越广泛了,那么今天我们就来说一说用同样的手法实现边框阴影. 一.边框阴影box-shadow 边框 ...
- Android 关于解决MediaButton学习到的media控制流程
问题背景:话机连接了头戴式的耳机,在通话过程中短按按钮是挂断电话,长按按钮是通话静音.客户需求是把长按改成挂断功能,短按是静音功能. android版本:8.1 在通话中,测试打印信息,可以看到but ...