Promise 学习笔记

作者: fejavu | 来源:发表于2019-07-02 22:00 被阅读80次

    回调地狱

    首先有一个需求,如何连续根据函数的依赖关系,实现多个函数的连续调用,而且要在前置函数完成的情况下。例如 1 秒钟之后执行 fn1fn1 执行完毕,相隔 1 秒,执行 fn2fn2 执行完毕,相隔 1 秒,执行 fn3

    我们可以利用回调函数,将后续需要执行的函数作为前置函数的回调函数参数,在前置函数执行之后执行。

    // exp 1
    function fn1(callback) {
      setTimeout(()=>{
        console.log('fn1 executed');
        callback();
      },1000);
    }
    
    function fn2(callback) {
      setTimeout(()=>{
        console.log('fn2 executed');
        callback();
      },1000);
    }
    
    function fn3() {
      setTimeout(()=>{
        console.log('fn3 executed');
      },1000);
    }
    
    fn1(function() {
      fn2(function() {
        fn3();
      });
    }); 
    // "fn1 executed"
    // 1s~
    // "fn2 executed"
    // 1s~
    // "fn3 executed"
    

    上述代码中不断嵌入回调函数,回调函数中还有函数作为参数,结果输出没有问题。但是代码缺乏可读性和拓展性,健壮性。当其中一个函数需要修改,或者嵌套回调层数增多,将陷入常说的“回调地狱”中,我们需要一种更为符合逻辑,更优雅的异步回调的方法—— Promise。

    Promise 的含义
    MDN 文档 中,它是被这样定义的:

    Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。

    Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 Promise 对象

    阮一峰老师的 理解

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

    Promise 通过对构造函数的设计,让异步编程比传统的方式更合理,处理更为逻辑化,代码也更加优雅。

    语法

    Promise 对象具有两个特点:1)Promise 对象分别有三种状态 pendingfullfiledrejected。最开始时是pending,当内部异步函数执行成功,则状态立即变为fullfilled且不可更改;当内部异步函数执行失败,则状态立即变为rejected,且不可更改。2)Promise 对象定义后会立即执行,而 resolve 函数要等待响应的结果。

    // exp 2
    const promiseExp  = new Promise(function(resolve, rejected) { 
      if(/*success condition */) {
        resolve(value);
      }else {
        reject(error);
      }
    });
    
    function ifSuccess() {
      // do somthing if success
    }
    
    function ifFailure(error) {
      // do somthing if fail
    }
    promiseExp().then(ifSuccess).catch(ifFailure(error));
    

    上述代码中,当 resolve 或者 reject 被执行,Promise 状态从pending(进行中) 变成fullfilled(完成)或者 rejected(失败);当其中任意一个发生时,就会调用相对应的函数,成功后该执行的函数或失败后该执行的函数,且由于.then().catch方法依旧返回一个 Promise 对象,::因此可以链式调用,类似于解决文章开头的“回调地狱”的问题。
    Promise 改造:

    // exp 3
    function fn1() {
      return new Promise(function(resolve, reject) {
        console.log('fn1 promise immediatly');
        setTimeout(()=>{
          console.log('fn1 async');
          resolve();
        },1000);
      });
    }
    
    function fn2() {
      return new Promise(function(resolve, reject) {
        console.log('fn2 promise immediatly');
        setTimeout(()=>{
          console.log('fn2 async');
          resolve();
        },1000);
      });
    }
    
    function fn3() {
      return new Promise(function(resolve, reject) {
        console.log('fn3 promise immediatly');
        setTimeout(()=>{
          console.log('fn3 async');
          resolve();
        },1000);
      });
    }
    
    function onerror() {
      console.log('error');
    }
    
    fn1().then(fn2).then(fn3).catch(onerror);
    console.log('outer after calling');
    /*
    "fn1 promise immediatly"
    "outer after calling"
    1s~
    "fn1 async"
    "fn2 promise immediatly"
    1s~
    "fn2 async"
    "fn3 promise immediatly"
    1s~
    "fn3 async"
    */
    

    上述代码对最初的代码进行了改造,我们可以看到几点:
    1)通过状态的变化,触发.then()函数中的函数,我们避免了多层的回调函数嵌套,以同步的方式进行异步函数回调,更具有可读性,合理性和健壮性。
    2)Promise 对象是立即执行的,体现在1s的间距,"fn1 immediatly" 是和 “outer after calling ”一起输出的,而其他的都是上一个异步函数(fnx async )和 异步完成调用函数(fnx+1 promise immediatly)一起输出的。
    3).then()返回的仍是一个 Promise 对象,因此在连续的回调函数依赖关系中,通过对 promise.prototype.then 的连续链式调用,实现了连续的函数回调(如下图)。

    Promise 的链式调用

    Promise 的原型

    Promise.prototype.then()

    添加解决(fulfillment)和拒绝(rejection)回调到当前 Promise, 返回一个新的 Promise, 将以回调的返回值来resolve

    then(onfulfilled, onrejected)then()有两个参数,一个是 resolve 状态的回调函数,一个是 reject状态的回调函数(可选),分别对应onfullfilledonrejected;根据上面的例子,then返回的仍是 Promise 对象,因此可以实现链式调用。

    // exp 4
    getIp("/getIp.php").then(
      ipResult => getCity(ipResult.ip)
    ).then(
      city => getWeather(city),
      err => console.log('rejected: ' + err);
    )
    

    上面的代码中,第一个then()指定的回调函数getCity返回的仍是一个 Promise 对象,因此继续调用then(),此时,如果第二个then指定的回调函数就会等待新的返回的 Promise 对象状态的变化,如果是状态变为 resolved,则执行getWeather,如果是rejected,则执行console.log('rejected: '+err);

    Promise.prototype.catch

    then()一样,catch()方法返回的是一个 Promise 对象,以及他拒绝的理由,他的行为和Promise.prototype.then(undefined, onRejected)。实际上,ECMA 的 官方文档 就是这么写的:obj.catch(onRejected)等同于obj.then(undefined, onRejected)

    promise.prototype.catch from ECMA
    catch 的使用中,他不仅可以捕获来自源 Promise 对象抛出的错误(下面第一个例子),也同时可以捕获在链式调用thencatch时,由then抛出的错误(第二个例子)。
    // exp 5
    const promise = new Promise(function(resolve, reject) {
      console.log('before throw');
      throw new Error('error test');
      console.log('after throw');
    });
    promise.catch(function(error) {
      console.log(error);
    });
    //  "before throw"
    //  "error test"
    

    上面的代码中,我们定义了一个 Promise 对象,他的作用就是抛出一个错误,并将错误的内容传递出去,当 Promise 对象调用catch捕获的时候,它可以直接捕获由 Promise 传递出的 error,并且立即执行完毕throw,错误被捕捉之后,就不再执行之后的函数,因此在throw之后的log语句没有被执行出来。

    // exp 6
    const promise = new Promise(function(resolve,reject) {
      resolve('error test');
    });
    
    promise.then(function(err){
      console.log('now I throw an error');
      throw new Error(err);
    }).catch(function(err){
      console.log(err);
    });
    //  "now I throw an error"
    //  "error test"
    

    上面的代码和前一段不同,在 Promise 对象中,并没有抛出错误。错误时在then的回调函数中抛出的,可以看到,catch不仅可以捕获来自第一个 Promise 的错误,由于链式调用的原因,还可以捕获then()回调函数返回的 Promise 对象的错误。

    前面说道:

    Promise 对象具有两个特点:1)Promise 对象分别有三种状态 pendingfullfiledrejected。最开始时是 pending,当内部异步函数执行成功,则状态立即变为fullfilled且不可更改;当内部异步函数执行失败,则状态立即变为rejected且不可更改

    如果是已经执行resolve之后,状态变成了fullfilled,再抛出错误,会不会被catch捕获呢?

    // exp 7
    const promise = new Promise(function(resolve,reject) {
      resolve('The ink is dry');
      throw new Error('An error after resolve');
    });
    
    promise.then(function(msg){
      console.log(msg);
    }).catch(function(err){
      console.log(err);
    });
    // "The ink is dry"
    

    不会,因为 Promise 的状态已经从pending变成fullfilled,就不会改变,同理如 exp 5 的 Promise 对象中的 console.log语句,在throw语句之前的log被执行了,之后的log没有被执行,因为throw之后,Promise 的状态已经改变了,就不会再继续执行下面的代码。

    Promise.prototype.finally

    finally()方法返回一个 Promise。在 Promise 结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()catch()中各写一次的情况。

    // exp 8
    promise()
    .then(val =>{/*  do something success */})
    .catch(err =>{/*  do something fail */})
    .finally(() => {/*  do something whatever*/})
    

    上面使用的案例中,我们规定了成功该做什么是,并传入一个val值,规定了失败该做什么时,并传入错误原因,最终,我们无论成功失败,都要完成的事情,它并没有输入的参数,也无法从它确定Promise 的状态。

    阮一峰老师试着实现了了finally函数

    // exp 9
    Promise.prototype.finally = funtion (callback) {
      let P  = this.constructor;
      return this.then(
        val => P.resolve(callback()).then( () => val),
        err => P.resolve(callback()).then( () => err)
      );
    }
    

    Promise 的方法

    Promise.all

    .all方法的参数是一个 Promise 对象列表,而返回的仍是一个Promise 对象,当输入的所有的 Promise 对象状态都为resolved时,返回的 Promise 新对象才返回resolve,当有一个出现reject时,则新返回的 Promise 返回reject,错误原因是第一个出现失败的 Promise 的结果。

    // exp 10
    var promise1 = Promise.resolve(3);
    var promise2 = 42;
    var promise3 = new Promise(function(resolve, reject) {
      setTimeout(resolve, 100, 'foo');
    });
    
    Promise.all([promise1, promise2, promise3]).then(function(values) {
      console.log(values);
    });
    // expected output: Array [3, 42, "foo"]
    

    上面代码中,promise.all等待输入的三个 Promise 均完成后,尽管一些 Promise 没有包含异步函数,但是结果其结果仍然被放进了最终返回的 Promise 中。

    在作为参数列表的 Promise 对象中,如果他有自己的catch函数,当他抛出错误时,他的错误将被自己的catch捕获,而不会被promise.allcatch捕获。

    //  exp 11
    let p1 = new Promise(function(resolve, reject) {
      resolve('p1 is ok');
    }).then(result => result);
    
    let p2 = new Promise(function(resolve, reject) {
      throw new Error('error test');
    }).then(function(msg) {
      console.log(msg);
    }).catch(function(err){
      console.log('p2 err captrue: '+ err);
    });
    
    let promise = Promise.all([p1,p2]);
    promise.then(function(msg){
      console.log('promiseAll msg: '+msg);
    }).catch(function(err){
      console.log('promiseAll err captrue: '+err);
    });
    /*
    "p2 err captrue: Error: error test"
    ["p1 is ok", undefined]
    */
    

    上面的代码中,p1 状态为resolved,并将resolve的值"p1 is ok"作为结果传入返回的回调函数中;p2 则抛出了一个错误,但是这个错误被 p2 本身的catch函数捕捉到了,catch函数捕捉到之后,返回一个新的 Promise,此时这个 Promise 的状态是resolved,因此,当使用 Promise.all([p1, p2])的时候,两者的状态都为resolved,只是 p2 没有返回的值,因此输出中,p2 的值是"undefined",如果 p2 没有自己的catch方法,则在Promsie.all([p1,p2])中则会调用catch方法。

    Promsie.race

    Promise.racePromise.all方法输入的参数一致,都是一个参数数组,只是.race是一旦参数数组中的某一个 Promsie 完成(resolve)或者拒绝(reject),状态更改,他就会返回一个新的Promsie,状态和参数列表中的第一个发生状态改变的 Promsie 一致。

    //  exp 12
    var p1 = new Promise(function(resolve, reject) {
      setTimeout(resolve,100,'promise one 100ms');
    });
    
    var p2 = new Promise(function(resolve, reject) {
      setTimeout(resolve,200,'promise two 200ms');
    });
    
    var promise = Promise.race([p1,p2]).then(function(result){
      console.log(result);
    });
    // ""promise one 100ms""
    

    上面的代码中,设置了两个 Promise 对象参数,但是设置了不同的异步完成的时间,p1 比 p2 快 100ms,因此在 p1 状态发生改变,从pendingresolved之后,Promise.race立即返回新的Promise对象,状态和 p1 一直,传递的值就是 p1 的值。

    Promise.resolve

    Promise.resolve方法返回一个给定解析值的 Promise 对象,也就是将现有对象转化为 Promise 对象。传入的参数可以是一个 Promise 对象,也可以是一个 thenable

    静态使用 resolve方法

    //  exp 13
    Promise.resolve('resolve exp').then(function(msg){
      console.log(msg);
    },function(err){
      console.log('Error:'+err); // 不会执行
    });
    // "resolve exp"
    

    上面代码中,resolve 方法直接返回一个新的 Promise 对象,并且处于 fullfilled 状态,携带的 value是 "resolve exp" 因此,新的 Promise 对象直接调用.then方法。

    参数是一个thenable对象

    //  exp 14
    let thenable = {
      then:function(resolve, reject) {
        resolve('resolved before throw');
        reject('after resolve');
      }
    };
    
    var p = Promise.resolve(thenable);
    p.then(function(msg) {
      console.log(msg);
    },function(err){
      console.log('error: ' + err);
    });
    

    上面的代码中,resolve输入的是一个 thenable对象,resolve方法会将这个对象转为 Promise 对象,然后执行thenable对象的then方法,执行后p的状态将变为resolved,因此pthen方法将会被执行,输出thenable传递的msg

    需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

    //  exp 15
    setTimeout(function () {
      console.log('three');
    }, 0);
    
    Promise.resolve().then(function () {
      console.log('two');
    });
    
    console.log('one');
    /*
    "one"
    "two"
    "three"
    */
    

    上面的代码中,setTimeout()是在下一轮时间循环开始时执行,Promise在本轮时间循环结束时执行,console.log立即执行,因此最先输出。

    Promise.reject

    本方法返回一个带有拒绝原因的Promise对象,该对象的状态自然为rejected

    静态使用reject方法

    // exp 16
    Promise.reject('reject exp').then(function(reason) {
      console.log(reason)}, 
    function(reason) {
      console.log('Error: ' + reason) ;
    });
    //Error: reject exp
    

    上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

    resolve方法不同的是,reject方法传入的参数,会作为后续方法的理由,而不是像resolve一样,将原 thenable 传入的参数传递。

    // exp 17
    let thenable = {
      then:function(resolve, reject) {
        reject('reject exp');
      }
    };
    
    var p = Promise.reject(thenable);
    
    p.then(null,function(e){
      console.log('target: ' + e);
    });
    // "target: [object Object] "
    

    上面函数中,传递到p.then中的参数不是"reject exp"字符串,而是thenable对象本身。

    await async

    await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用。使用 Promise 配合 await 和 async,我们已经可以像书写同步函数那样书写异步函数。

    // exp 18
    function resolve2second(x) {
      return new Promise(resolve => {
        setTimeout(()=>{
          resolve(x);
        },2000);
      });
    };
    
    async function fn1() {
      console.log('fn1 immediatly');
      var x = await resolve2second(10);
      console.log(x)
    }
    
    fn1();
    /*
    "fn1 immediatly"
    // 2s~
    10
    */
    

    上面代码中,resolve2second是一个2秒后执行的异步函数,在async 函数fn1中,设置了await表达式,使得x变量赋值的操作暂停,等待Promise结果出来后,由返回的resolve值再执行对x的赋权,而fn1函数的内的console.log函数不受影响,随fn1立即执行

    参考阅读

    1. Promise 对象,阮一峰。
    2. Promiseawaitasync_function,MDN。

    相关文章

      网友评论

        本文标题:Promise 学习笔记

        本文链接:https://www.haomeiwen.com/subject/uyjmcctx.html