JavaScript异步回调---Promise 详解

305 阅读7分钟

回调地狱(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一般是同步使用的,两者缺一不可,