zoukankan      html  css  js  c++  java
  • 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);//1
    },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));//42

    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)) //1
        .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);//3
    });

    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执行机制相关有疑问,可以阅读博主这篇博文

    JS执行机制详解,定时器时间间隔的真正含义

    欢迎大家留言讨论!!!

  • 相关阅读:
    数据结构与算法JavaScript (一) 栈
    js架构设计模式——前端MVVM框架设计及实现(二)
    js架构设计模式——前端MVVM框架设计及实现(一)
    js架构设计模式——MVC,MVP 和 MVVM 的图示及简单明了的区别说明
    js架构设计模式——你对MVC、MVP、MVVM 三种组合模式分别有什么样的理解?
    js面向对象oop编程
    js模块化开发——前端模块化
    SPRING 集成 activemq 的 topic 模式
    linux yum 本地源配置
    ORACLE 导入的问题
  • 原文地址:https://www.cnblogs.com/echolun/p/10693470.html
Copyright © 2011-2022 走看看