回调地狱(callback hell)
回调地狱概念图:

回调地狱:在我们需要对一个异步操作进行频繁的调用的时候,且要保证一步操作的顺序,可能会出现.
var fs=require('fs');
/* 注意这里的fs.readFile是一个异步任务,所以这里他们输出的顺序并不是
按照代码的书写顺序,他们读取文件的输出顺序,跟文件资源的大小还有其他的元素有很大的关系
*/
fs.readFile('./data/a.txt','utf8',function(err,data){
if(err){
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
});
fs.readFile('./data/b.txt','utf8',function(err,data){
if(err){
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
});
fs.readFile('./data/c.txt','utf8',function(err,data){
if(err){
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
});
通过回调嵌套的方式保证顺序:
var fs = require('fs');
/* 注意这里的fs.readFile是一个异步任务,所以这里他们输出的顺序并不是
按照代码的书写顺序,他们读取文件的输出顺序,跟文件资源的大小还有其他的元素有很大的关系
*/
fs.readFile('./data/a.txt', 'utf8', function (err, data) {
if (err) {
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
fs.readFile('./data/b.txt', 'utf8', function (err, data) {
if (err) {
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
fs.readFile('./data/c.txt', 'utf8', function (err, data) {
if (err) {
/* 抛出异常
1.阻止程序的执行
2.把错误的消息打印到控制台 */
throw err
}
console.log(data);
});
});
});
回调地狱的缺点: 1)代码的可维护性非常差,不利于代码的阅读 2)层层嵌套,代码复杂.
Promise简介
为了解决以上编码方式带来的问题(回调地狱嵌套),所以在ECMAScript 6 中新增了一个API:promise Promise的英文就是承诺 保证的意思
概念 : Promise是ES6中的新语法,Promise是一个构造函数,每个new 出来的Promis实例对象都代表一个异步操作.
注意:使用promise并不会减少代码量
Promise概念图:

Promise语法介绍
简单创建:
// Promise是构造函数
// Promise.prototype上有.then() .catch .finally(),因为他绑定到了原型上,所以根据原型链的查找规则,他的实例对象也可以使用这个方法
// Promise表示异步操作
// 下面的这个代码表示创建了一个形式上的异步操作
// 通过new Promise()的时候,提供了一个function函数,在function函数中,可以执行具体的异步操作
const p=new Promise(function(){
// 在这个function中可以执行具体的异步操作
// 比如读文件,或发送ajax
// fs.readFile()
})
Promise.prototype上的方法
Promise.prototype.then():
const fs=require('fs');
// 在ES6中新增了一个API Promise
// Promise 是一个构造函数
// 创建promise容器
// 1.给别人一个承诺
// Promise容器一旦创建,就开始执行里面的代码,
// 承诺本身不是异步的,但是内部往往都是封装一个异步任务
var p1=new Promise(function(resolve,rejected){
fs.readFile('./data/a.txt','utf8',function(err,data){
if(err){
// 失败了,承诺容器中的任务失败了
// console.log(err);
// 把容器的Pending状态改变为Rejected
//调用reject就相当于调用了then方法的第二个参数
rejected(err);
}else{
// 承诺容器中的任务成功了
// console.log(data);
// 把容器的Pending状态改为Resolved
// 也就是说这里调用的resolve方法实际上就是then方法传递的function
resolve(data);
}
});
});
// p1就是那个承诺
// 当p1成功了然后(then)做指定的操作
// then方法接收的function就是容器中的resolve
p1.then(function(data){
console.log(data);
},function(err){
console.log('读取文件失败了...',err);
})
Promise.prototype.catch()
Promise.prototype.catch()方法是.then(null,rejection)或者是.then(undefined,rejection)的别名,用于指定发生错误时的回调函数跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
Promise.all()
promise.all方法用于将多个promise实例,包装成一个新的promise实例,
promise.all方法接收一个数组作为一个参数,他的每一项都是一个promise对象
另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator
接口,且返回的每个成员都是 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p的状态由p1,p2,p3决定,分为两种情况
(1) 只有p1,p2,p3的状态都变成了fulfilled,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的回调函数
(2)只要p1,p2,p3之中有一个被rejected,p的状态就变成了reject,此时第一个被reject的实例的返回值,会传递给p的回调函数
Promise.all = arr => {
let aResult = []; //用于存放每次执行后返回结果
return new _Promise(function (resolve, reject) {
let i = 0;
next(); //开始逐次执行数组中的函数
function next() {
arr[i].then(function (res) {
aResult.push(res); //执行后返回的结果放入数组中
i++;
if (i == arr.length) { //如果函数数组中的函数都执行完,便把结果数组传给then
resolve(aResult);
} else {
next();
}
})
}
})
};
里面的function next()看起来像是一个循环,但实际上是一个递归调用,只有数组前一个执行完了,
才能执行下一个,如果用循环的话,无法控制下一个的执行。
在这里有一个要点,也是Promise.all本身有的,传进Promise.all的数组元素,
必须都是一个Promise对象,否则是无法实现调用的
Promise.race()
Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。
那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,
就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,
再进一步处理。
Promise.resolve()
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
封装Promise版本的ReadFile:
const fs = require('fs');
function pReadFile(filePath){
return new Promise(function (resolve, rejected) {
fs.readFile('./data/a.txt', 'utf8', function (err, data) {
if (err) {
rejected(err);
} else {
resolve(data);
}
});
});
}
pReadFile('./data/a.txt')
.then(function(data){
console.log(data);
return pReadFile('./data/b.txt');
})
.then(function(data){
console.log(data);
return pReadFile('./data/c.txt');
})
.then(function(data){
console.log(data);
})
//可以通过.catch方法,捕获前面所有的.then方法发生的错误,几种处理
.catch(function(err){
console.log(err.message)
})
Promise代码图示:

asyns、await对Promise的优化
Generator
- 说到async函数就不得不提起他在ES6中的表现,他其实就是Generator函数的一种语法糖
- Generator可以理解为一个状态机,他身上挂载了好多的状态,
- 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。
一是,function关键字与函数名之间有一个星号;(在async函数中就相当于是*)
二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)(在async函数中就相当于是await)
Generator函数必须调用next()方法才会调用下面的函数,但是在async函数中,已经对next函数进行了一些封装,此时我们不需要调用next函数就可以执行
ES7 中的 async 和 await 可以简化 Promise 调用,提高 Promise 代码的 阅读性 和 理解性;
// 如果某个方法内部用到了await关键字,那么这个方法必须被修饰为异步async方法
// await只能用在被async修饰的方法中
async function test(){
// 如果某个方法的返回值是Promise的实例对象,就可以用await修饰Promise实例
// await只能用在被async修饰的方法中
const data=await getContentPath('./files/1.txt').catch(err=>err);
if(data instanceof Error){
console.log('文件读取失败')
}else{
console.log(data);
}
console.log(data);
const data2=await getContentPath('./files/2.txt');
console.log(data2);
const data3=await getContentPath('./files/3.txt');
console.log(data3);
}
// 这是异步方法,但是并不是纯粹的异步方法
// 在异步方法中,遇到第一个await之前,所有的代码都是同步执行的
test();
注意:async和await一般是同步使用的,两者缺一不可,