ES6 已经学习了不少,因为平常学习写代码没怎么用到 Promise 就没怎么学这一部分。但随着深入的学习,接触好几次 Promise 了,决心学习一下,毕竟这部分还是很重要的,还有就是原生最新的 Ajax 请求,不用不知道有多好用,以前的设计真是反人类的设计。
一、Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
简单来讲 Promise 就是用来替换传统 ES5 的异步回调。
现在使用ES5 来造一个函数观察异步回调的坑。(回调黑洞)
function fun(ms,callback){
setTimeout(callback,ms);
}
fun(2000,function(){
console.log("Condor Hero");
});
函数执行两秒钟后打印内容。现在来看函数书写的没有任何不方便之处,最起码看起来结构清晰。
但是如果我要求每隔两秒依次打印出 Promise 是用来 替换 ES5 异步函数回调的 这句话。
function fun(ms,callback){
setTimeout(callback,ms);
}
fun(2000,function(){
console.log("Promise");
fun(2000,function(){
console.log("是用来");
fun(2000,function(){
console.log("替换");
fun(2000,function(){
console.log("ES5");
fun(2000,function(){
console.log("异步函数回调的");
})
})
})
});
});
这时候再来看这个代码结构,就没有上面的简洁了,整体代码给人一种很凌乱的感觉,虽然我写的并不乱。回调语句过多就会发现自己深深的陷入了回调黑洞。这时候 Promise 的优点就体现出来了。
function fun(ms){
return new Promise(function(resolve,reject){
// 内容直接执行
setTimeout(resolve,ms);
});
}
fun(2000).then(function(){
console.log("Promise");
}).then(function(){
fun(2000).then(function(){
console.log("是用来");
});
}).then(function(){
fun(4000).then(function(){
console.log("替换");
});
}).then(function(){
fun(6000).then(function(){
console.log("ES5");
});
}).then(function(){
fun(8000).then(function(){
console.log("异步函数回调的");
});
});
看看结构是不是更好一点那?我认为是没有啦,感觉和ES5没啥区别,代码多了一样会进入回调黑洞里面。不过代码逻辑结构可能比 ES5 会好点,由原来的松散结构变成链式打点法。不过这种方法倒是很有高大上的感觉。既然是未来的标准还是的学学。
而且这里有个名词的理解一下:语法糖
计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Promise 就是异步回调的语法糖,这种语法对语言的功能并没有影响,增加了代码的可读性,减少程序代码出错的机会。
所以说:Promise 本质是 JavaScript 异步回调的语法糖。
二、Promise 基本使用
首先来看看这个构造函数身上到底有什么,我们通过下面代码在控制台输出。
function fun(ms){
return new Promise(function(resolve,reject){});
}
console.log(fun());
Promise.png
上图返回了 Promise 的状态 pendding ,Promise 上绑定原型函数有catch,reject,resolve,then等。
现在来看看 Promise 的基本结构。
function fun(){
return new Promise(function(resolve,reject){
resolve("我的名字是: Condor Hero");
});
}
fun().then(function(value){
console.log(value);
});
Promise 这个构造函数必须用执行函数的 return 返回。返回的 resolve 和 reject,分别代表成功和失败。resolve 和 reject 是两个函数绑定在 Promise 的原型上的,当 fun 函数执行可以使用 then 来接收,then 这个方法有两个参数,这两个参数都是函数,第一个是响应 resolve 函数的,第二个是响应 reject 函数的。
三、Promise 异步加载图片
function loadImage(){
return new Promise(function(resolve,reject){
const img = new Image();
img.onload = function(){
resolve(img);
}
img.onerror = function(){
reject(new Error("图片加载失败,请检查重试!"));
}
img.src = "./5.jpg";
});
}
loadImage().then(function(img){
// 节点上树
document.documentElement.appendChild(img);
});
异步加载完成
四、Promise对象实现 Ajax 操作。
const getJSON = function(url) {
return new Promise(function(resolve, reject){
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status === 200 || this.status.toString().indexOf(3) == 0) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
};
xhr.open("get",url,true);
xhr.send();
});
};
getJSON("/index.html").then(function(json) {
console.log(`请求内容为:${json}`);
}, function(error) {
console.error('请求出错了', error);
});
五、Promise 的 catch() 方法
Promise 的catch() 方法和try{}catch(){} 非常像,这里先复习一下 try{}catch(){}
执行下面的语句,会发现一旦前面出错,后面的语句没法再执行了。
add();
console.log("程序前面出错,就看不到我!");
程序前面出错,就看不到我
如果改成这样写。
try{
//里面是执行语句
add();
}catch(err){
// err是错误信息
// 捕获到错误执行语句
console.log(err);
}
console.log("程序前面出错,就看不到我!");
错误就会被捕获,从而不影响后面的语句执行,当然 catch 里面也可以不打印错误,这样错误就不会出现。
由此可知 try{}catch(){} 用于开发者已知某地方出错,为了不让错误影响后面代码执行而提供的。
错误被捕获
重点:
try..catch..虽然能捕获错误,但是不能捕获异步的异常;
try{
setTimeout(function(){
throw new Error("错误");
},2000);
}catch(err){
console.log("BBB");
}
console.log("A");
BBB是不能输出的,控制台还是会收到错误信息。
ES6 中 Promise 对象的实例提供了 catch() 方法,表示异步捕获异常。
用上面的 Promise 异步加载图片来说明Promise.prototype.catch 的用法。
function fun(){
return new Promise(function(resolve,reject){
const img = new Image();
img.onload = function(){
resolve(img);
}
img.onerror = function(){
reject(new Error("图片加载失败,请重新检查!"));
}
img.src = "./5.jpg";
});
}
fun().then(function(img){
// 故意把documentElement 的 e 改成小写
document.documentelement.appendChild(img);
}).catch(err => console.log(err));
打印错误
Promise.prototype.catch 方法是用于指定发生错误时的回调函数,上面代码中,fun() 方法返回一个 Promise 对象,如果该对象状态由 pending 变为 resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为 rejected,就会调用 catch 方法指定的回调函数,处理这个错误。另外,then 方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
六、Promise.prototype.finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
fetch("index.html").then(res => res.text()).then(res => console.log(res)).catch(err => console.log(err)).finally(() => console.log("最终,我会被执行!"));
上面代码中,不管 promise 最后的状态,在执行完 then 或 catch指 定的回调函数以后,都会执行 finally 方法指定的回调函数。
七、Promise.all()
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
只要 p1、p2、p3 之中有一个被rejected,p的状态就变成rejected
总结起来就是一假全假
接下来我们用上面做的 Ajax 来打开五个 TXT 文件,每个 TXT 文件有一句古诗。
const getJSON = function(url) {
return new Promise(function(resolve, reject){
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status === 200 || this.status.toString().indexOf(3) == 0) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
};
xhr.open("get",url,true);
xhr.send();
});
};
// 生成一个Promise对象的数组
const promises = [1,2,3,4,5].map(item=>getJSON(`./${item}.txt`));
Promise.all(promises).then((posts)=>posts.forEach(item => console.log(item))).catch((err)=>console.log(err));
鹊桥仙
五个全部请求成功,但是只要有一个请求未成功,Promise.all 这个实例都会去 catch 捕获错误。
什么是 iterator ?
1、遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
2、Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
3、在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for...of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
4、在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。
八、Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例 率先改变状态,p 的状态就跟着改变(可以使用延迟器来调整率先改变顺序)。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
如果把上一个鹊桥仙 案例中的Promise.all 部分改成:
Promise.race(promises).then((posts)=>console.log(posts)).catch((err)=>console.log(err));
注意:返回结果不是数组了。
谁先成功结果
九、理论补充
学东西我还是喜欢实践,所以先搞明白怎么用的,在学理论深入理解一下。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。