深入学习JavaScript中的promise

目录
  • 为什么要用Promise?
  • 使用Promise解决异步控制问题
  • Promise的结构
    • 回调函数
    • 为什么异步代码一定是回调函数结构?
    • 刨析Promise
    • 原型方法——catch\finally\then
    • 为什么要在.then的回调函数中return一个Promise呢?
    • 那如果我们不指明return返回值,它会返回什么呢?是如何实现链式调用呢?
    • resolve和reject
    • resolve()
    • reject()
    • Promise常用API——all()、allSettled()、any()、race()
      • all()
      • race()
      • allSettled()
    • any()

为什么要用Promise?

我们知道JavaScript是单线程的,一次只能执行一个任务,会阻塞其他任务。因此,所有的网络任务、游览器事件等都是异步的,我们可以使用异步回调函数来进行异步操作。

有这么一个场景,我可以通过6个人能够认识到任何一个人。但是我们不知道当前的人联系到下一个人的时间是多久,假如这是一个异步的操作。

可以用如下代码表示:

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  let i = 1;
  setTimeout(() => {
    const result1 = ConnectPeople(i);
    setTimeout(() => {
      const result2 = ConnectPeople(result1);
      setTimeout(() => {
        const result3 = ConnectPeople(result2);
        setTimeout(() => {
          const result4 = ConnectPeople(result3);
          setTimeout(() => {
            const result5 = ConnectPeople(result4);
            setTimeout(() => {
              const result6 = ConnectPeople(result5);
              setTimeout(() => {
                const result7 = ConnectPeople(result6);
                setTimeout(() => {
                  const result8 = ConnectPeople(result7);
                  setTimeout(() => {
                    const result9 = ConnectPeople(result8);
                    setTimeout(() => {
                      const result10 = ConnectPeople(result9);
                    }, 10000);
                  }, 5000);
                }, 3000);
              }, 2000);
            }, 3000);
          }, 2000);
        }, 1000);
      }, 500);
    }, 2000);
  }, 1000);

如上所示,当我们联系到了第一个人后,再去联系第二个人,然后再去联系第三个人...直到我联系到了10个人。乍一看,代码好像还挺规整,但是如果100个人,1000个人呢?由于回调很多,函数作为参数层层嵌套,就陷入了回调地狱。这种情况下,就像是金字塔一样的代码非常不利于阅读。

但是还好,我们有解决办法。

使用Promise解决异步控制问题

什么是Promise?

Promise对象的主要⽤途是通过链式调⽤的结构,将原本回调嵌套的异步处理流程,转化成“对象.then().then()...”的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调⽤的结构,这样就可以在阅读上编程上下左右结构的异步执⾏流程了。

因此,Promise的作⽤是解决“回调地狱”,他的解决⽅式是将回调嵌套拆成链式调⽤,这样便可以按照上下顺序来进⾏异步代码的流程控制。 如下代码所示,我们使用了Promise,代码也从原先的金字塔形式转变成了从上往下的执行流程。

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  const p = new Promise((resolve) => {
    setTimeout(() => {
      resolve(ConnectPeople(1));
    }, 1000);
  });
  p.then((v1) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v1));
      }, 1000);
    });
  }).then((v2) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v2));
      }, 1000);
    });
  });

Promise的结构

根据上面的代码案例,我们发现Promise需要通过new关键字同时传入一个参数来创建,所以我们可以尝试打印一下window对象console.log(window)(window 对象在浏览器中有两重身份,一个是ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口),可以发现存在一个Promise的构造函数。

Promise初始化的时候需要传入一个函数,如下所示:

const p = new Promise(fn) // fn是初始化的时候调用的函数,它是同步的回调函数

回调函数

什么是回调函数?

JavaScript中的回调函数结构,默认是同步的结构。由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。

为什么异步代码一定是回调函数结构?

我们知道JavaScript是单线程异步模型,严格按照同步在前异步在后的顺序执行。如果用默认的上下结构,我们拿不到异步回调中的结果。

如下所示,代码执行的时候会先执行同步代码,异步代码会挂起,继续执行同步代码,到1s的时候挂起的任务会进入队列,到2s的时候会继续执行同步代码打印1,然后从任务队列中取任务将num变成100,打印num。 所以实际执行效果是,过2秒后,先打印1再打印100。

var num = 1;
setTimeout(()=>{
    num = 100
    console.log(num)
},0)
var d = new Date().getTime()
var d1 = new Date().getTime()
while ( d1 - d < 1000 ) {
    d1 = new Date().getTime()
}
console.log(num) // 1

刨析Promise

翻译一下promise,结果是承诺,保证,红宝书中的解释是期约

它有三个状态:

  • pending 待定,初始状态
  • fulfilled 兑现,已完成,通常代表成功执行了某一任务。初始化函数中的resolve()执行时,状态就会变味fulfilled,而且.then函数注册的回调会开始执行,resolve中传递的参数会进入回调函数成为形参。
  • rejected 拒绝,通常代表执行一次任务失败,调用reject()时,catch注册的函数就会触发,并且reject中传递的内容会变成回调函数的形参。

三种状态之间的关系:

当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏。

我们打印一下Promise对象,发现它的构造函数中定义了allallSettledanyracerejectresolve方法(这些是实例方法),它的原型上存在catchfinallythen方法(这些是原型方法)。

原型方法——catch\finally\then

首先看下面代码:

  new Promise(function (resolve, reject) {
    resolve();
    reject();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

执行后依次打印then执行->finally执行,发现.catch的回调没有执行。

再看如下代码:

  new Promise(function (resolve, reject) {
    reject();
    resolve();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

这个串代码和之前的代码唯一的不同在于Promise中的回调先执行了resolve()还是先执行了reject(),打印结果是catch执行->finally执行,发现.then的回调没有执行。

那如果Promise的回调不执行reject()和resolve()呢?

会发现什么输出都没有!

注意:Promise.prototype.catch()其实是一个语法糖,相当于是调用 Promise.prototype.then(null, onRejected)。.then中其实是可以传入2个回调函数,第一个回调函数是resolve()后执行,第二个回调函数是reject()后执行,2个是互斥的。

这是因为Promise的异步回调部分如何执⾏,取决于我们在初始化函数中的操作,并且初始化函数中⼀旦调⽤了resolve后⾯再执⾏reject也不会影响then执⾏,catch也不会执⾏,反之同理。

⽽在初始化回调函数中,如果不执⾏任何操作,那么promise的状态就仍然是pending,所有注册的回调函数都不会执⾏。

由此可见,执行完resolve()之后才能够执行.then的回调;执行reject()之后才能够执行.catch的回调;finally()的回调会在执行完.then或.catch之后执行。

这时候,我们就会想,是不是可以把resolve或者reject的调用设定在异步函数内去调用,这样是不是就能解决回调地狱的问题了?

所以我们就去尝试一下:

  new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(111);
      resolve();
    }, 2000);
  })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(222);
          resolve();
        }, 2000);
      });
    })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(333);
          resolve();
        }, 2000);
      });
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

上面代码每隔2s依次打印111->222->333 finally执行。333执行后立马执行finally。

为什么要在.then的回调函数中return一个Promise呢?

因为下一个异步的执行,需要等待前一个异步执行完毕后才调用,我们需要用到resolve来控制.then执行的时机。

那如果我们不指明return返回值,它会返回什么呢?是如何实现链式调用呢?

看下面代码:

  const p2 = new Promise((resolve, reject) => {
    resolve();
  });
  const p3 = p2.then(() => {
    console.log("resolved");
  });
  console.log(p3, 111);

p2.then的回调函数中没有return,但是我们知道一般来说函数返回值默认返回undefined,但是undefined中不会存在.then的方法。

因此我们就看一下p3里面到底是什么。有些人会想,.then是异步调用的,它是一个微任务,那访问p3是不是不太正确?

我们打印一下p3,就会看到如下信息:

第一行p3的状态还是pending,当我们点开,发现已经变成了fulfilled了,因为引用类型是按地址访问的,当我们点开的时候会发现指向这个地址里最后的数据是什么。普通对象同理。

如下所示,我们console的时候a对象的name还是a,但是我们点开后发现程序执行完后a对象的实际name变成了b。

  const a = { name: "a" };
  console.log(a);
  a.name = "b";

回归正传,我们发现p3里面有3个字段,[[Prototype]]我们很熟悉,这个是一个指向当前对象原型的指针。在大多数游览器中是可以通过__proto__访问到的。

我们尝试着去访问:

  console.log(p3, 111);
  console.log(p3.__proto__);
  console.log(p3.__proto__ === Promise.prototype); // true

我们可以看到.then默认返回的有3个字段,然后通过原型链来实现链式调用:

  • [[Prototype]]代表Promise的原型对象
  • [[PromiseState]]代表Promise对象当前的状态
  • [[PromiseResult]]代表Promise对象的值,分别对应resolve或reject传⼊的结果

本质就是在我们调⽤这些⽀持链式调⽤的函数的结尾时,他⼜返回了⼀个包含他⾃⼰的对象或者是⼀个新的⾃⼰,这些⽅式都可以实现链式调⽤。

中断链式调用的方式:

中断的⽅式可以使⽤抛出⼀个异常或返回⼀个rejected状态的Promise对象

链式调用的基本形式:

  • 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve传⼊的值
  • 后续每个函数都可以使⽤return返回⼀个结果,如果没有返回结果的话下⼀个then中回调函数的参数就是undefined
  • 返回结果如果是普通变量,那么这个值就是下⼀个then中回调函数的参数
  • 如果返回的是⼀个Promise对象,那么这个Promise对象resolve的结果会变成下⼀次then中回调的函数的参数
  • 如果then中传⼊的不是函数或者未传值,Promise链条并不会中断then的链式调⽤,并且在这之前最后⼀次的返回结果,会直接进⼊离它最近的正确的then中的回调函数作为参数

前面几条我们都能懂,第5条什么意思的? 看下面代码:

  const p2 = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
  });
  p2.then(() => {
    console.log(2);
    return 123;
  })
    .then()
    .then("456")
    .then((res) => {
      console.log(res);
    });

发现只打印了1 2 和 123,return的123进入了最后一个.then的回调函数中作为参数。

resolve和reject

至于resolve和reject,我们通过上面已经知道了resolve和reject能够更改Promise的状态,而Promise的状态是不可逆的,且是私有的。所以我们必须在Promise内部调用resolve或者reject。

当然,resolve和reject也能够传入参数,而传入的参数,会变为.then或.catch的回调函数中的参数。

那如果传入一个Promise作为参数呢???

resolve()

实际上,如果在resolve中传入一个promise,那它的行为就相当于是一个空包装。Promise.resolve()可以说相当于是一个幂等方法,会保留传入期约的状态。

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true

reject()

会实例化一个拒绝的期约并抛出一个异步错误,不能通过try...catch捕获,只能通过拒绝处理程序捕获。

如果给reject传入一个promise,则这个promise会成为返回的拒绝promise的理由。

  const p1 = new Promise(() => {});
  const p2 = Promise.resolve(111);
  const r3 = Promise.reject(p1);
  const r4 = Promise.reject(p2);
  console.log(r3);
  console.log(r4);

Promise常用API——all()、allSettled()、any()、race()

all()

假如我们有一个需求,一个页面需要请求3个接口才能渲染,并且要求3个接口必须全部返回。如果我们通过链式调用的方式,接口1请求了再去请求接口2然后去请求接口3,全都成功了再去渲染页面。这种就很耗时,所以就有了一个all的方法来解决。

Promise.all([promise对象,promise对象,...]).then(回调函数)

Promise.all()的参数是一个Promise数组,只有数组中所有的Promise的状态变成了fulfilled之后才会执行.then回调的第一个回调函数,并且将每个Promise结果的数组变为回调函数的参数。如果Promise中有一个rejected,那么就会触发.catch()的回调。

  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 1000);
  });
  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.all([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.log(err);
    });
    // 3s后打印 ['第⼀个promise执⾏完毕', '第三个promise执⾏完毕', '第⼆个promise执⾏完毕']

race()

race()方法与all()方法的使用格式相同,不同的是,回调函数的参数是promise数组中最快执行完毕的promise的返回值,它的状态可能是fulfilled也有可能是rejected,但是是最快返回的。

根据race这个单词就能理解,相当于一群promise进行比赛,谁先到终点第一就是谁,不管是男是女。

  //promise.race()相当于将传⼊的所有任务进行一个竞争
  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 5000);
  });

  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.race([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.error(err);
    });
    // 2秒后打印第二个promise执行完毕

allSettled()

该方法需要传入所有不在pendding状态的promise数组,然后通过该方法可以知道数组中的promise的当前状态。

当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个promise的结果时,通常使用它。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// "fulfilled"
// "rejected"

any()

这个方法目前还是实验性的,不是所有的游览器都能够支持。

接受一个promise数组,只要有一个promise的状态变成了fulfilled,那么这个方法就会返回这个promise;

如果所有的promise的状态都是rejected,那么就返回失败的promise,并且把单一的错误集合在一起。

const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});
const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});
const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
})
// 很快完成

到此这篇关于深入学习JavaScript中的promise的文章就介绍到这了,更多相关JavaScript promise内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JS Promise axios 请求结果后面的.then() 是什么意思

    目录 Promise 对象 Promise 对象的状态 回调函数 Promise.then() 绑定回调函数 使用 Promise:链式调用 链式调用的实现 错误处理 常见错误 创建 Promise 对象 Promise 其他静态方法 创建已决议的 Promise 对象 多个 Promise 对象 结语&参考文献 Promise 是JS中一种处理异步操作的机制,在现在的前端代码中使用频率很高.Promise 这个词可能有点眼生,但你肯定见过 axios.get(...).then(res =>

  • JS promise 的回调和 setTimeout 的回调到底谁先执行

    目录 任务 VS 微任务 执行过程 案例分析 结语 & 参考资料 首先提一个小问题:运行下面这段 JS 代码后控制台的输出是什么? console.log("script start"); setTimeout(function () { console.log("setTimeout1"); }, 0); new Promise((resolve, reject) => { setTimeout(function () { console.log(&

  • JavaScript中Promise的执行顺序详解

    目录 前言 代码分析 then 方法何时调用? 总结 前言 最近看到一个 Promise 相关的很有意思的代码: new Promise((resolve) => { console.log(1) resolve() }).then(() => { new Promise((resolve) => { console.log(2) resolve() }).then(() => { console.log(4) }) }).then(() => { console.log(3

  • javascript中Promise使用详解

    目录 一.首先,要知道为什么要用Promise语法? 二.接着,来了解一下回调地狱(Callback Hell) 三.最后,也是本章的重头戏,Promise的基本使用 (一) resolve函数 (二) rejected函数 (三)Promise的API 1. then 2. catch 3. finally 4. Promise.all 5. Promise.race 四.最后 前言: 做过前端开发的都知道,JavaScript是单线程语言,浏览器只分配给JS一个主线程,用来执行任务,但是每次

  • JavaScript中的Promise详解

    目录 Promise的基本用法: 1.创建Promise对象 2.Promise 方法 总结 Promise是异步编程的一种解决方案,是一个对象,可以获取异步操作的消息,大大改善了异步编程的困难,避免了回调地狱,比传统的解决方案回调函数和事件更合理和更强大. 从语法上讲,Promise是一个对象,它可以获取异步操作的消息.提供了一个统一的API,各种异步操作都可以用同样的方法进行处理 1.Promise的实例有三个状态: (1)Pending(进行中) (2)Resolved(已完成) (3)R

  • JavaScript手写Promise核心原理

    目录 准备 完善 resolve/reject then 异步处理 链式调用 边界处理 catch 优化后完整代码 准备 首先,promise 有三种状态:pending fulfilled rejected; promise在实例化操作中, 有两个改变状态的方法,分别为resolve,reject; promise有很多方法,详情请见 mdn, 本篇文章先实现 promise的核心api: then和catch; 我们使用 es6 提供的 class 来实现 class MyPromise {

  • Javascript的promise,async和await的区别详解

    终于把promise, async, await的区别和联系弄清楚了,看下面代码 写法1,2是promise的写法 写法6是async和await的写法 主要看第2种写法和第6中写法即可, 第2种写法是promise的典型写法,第6中写法是async, await的典型写法 // 以下三个请求依次执行 req1 = () => { return fetch("http://example.com/api/v1/get")} req2 = () => { return fet

  • 深入学习JavaScript中的promise

    目录 为什么要用Promise? 使用Promise解决异步控制问题 Promise的结构 回调函数 为什么异步代码一定是回调函数结构? 刨析Promise 原型方法——catch\finally\then 为什么要在.then的回调函数中return一个Promise呢? 那如果我们不指明return返回值,它会返回什么呢?是如何实现链式调用呢? resolve和reject resolve() reject() Promise常用API——all().allSettled().any().r

  • 关于javascript中的promise的用法和注意事项(推荐)

    一.promise描述 promise是javascript中标准的内置对象,用于表示一个异步操作的最终状态(是失败还是成功完成)及其结果值.它让你能够把异步操作最终成功或者失败的原因和响应的处理程序相关联,也就是说通过promise你可以自定义异步操作结束后该做什么.这样的话异步方法就和同步方法很类似,也有返回值,只不过这个返回值不是立即返回最终的值,而是返回一个promise,当promise的状态发生改变时,就会触发相应的处理程序. 一个promise无论什么时候都必然处于以下几种状态:1

  • 学习JavaScript中的闭包closure应该注意什么

    目录 闭包简述 1.闭包使得内部函数可以访问外部函数的属性(变量或方法) 2.闭包的广阔应用场景 3.用闭包模拟私有方法 4.从性能角度考虑,非必要不使用闭包 闭包简述 Mozilla 上这样解释闭包:一个函数和对其周围状态(lexical environment,词法环境) 的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure). 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域.在 JavaScript 中, 每当创建一个函数, 闭包就会在函数创建的同时

  • 深入学习 JavaScript中的函数调用

    定义 可能很多人在学习 JavaScript 过程中碰到过函数参数传递方式的迷惑,本着深入的精神,我想再源码中寻找些答案不过在做这件事之前,首先明确几个概念.抛弃掉值传递.引用传递等固有叫法,回归英文: call by reference && call by value && call by sharing 分别是我们理解的 C++ 中的引用传递,值传递.第三种比较迷惑,官方解释是 receives the copy of the reference to object

  • 深入学习JavaScript中的原型prototype

    javascript 是一种 prototype based programming 的语言, 而与我们通常的 class based programming 有很大 的区别,我列举重要的几点如下: 1.函数是first class object, 也就是说函数与对象具有相同的语言地位 2.没有类,只有对象 3.函数也是一种对象,所谓的函数对象 4.对象是按 引用 来传递的 那么这种 prototype based programming 的语言如何实现继承呢(OO的一大基本要素), 这也便是

  • JavaScript中的Promise使用详解

    许多的语言,为了将异步模式处理得更像平常的顺序,都包含一种有趣的方案库,它们被称之为promises,deferreds,或者futures.JavaScript的promises ,可以促进关注点分离,以代替紧密耦合的接口. 本文讲的是基于Promises/A 标准的JavaScript promises.[http://wiki.commonjs.org/wiki/Promises/A] Promise的用例: 执行规则 多个远程验证 超时处理 远程数据请求 动画 将事件逻辑从应用逻辑中解耦

  • 深入学习JavaScript中的bom

    BOM(Broswer Object Model) 凡是 window 的属性和方法,均可以省略"window." 方法: 框窗 1.警告框 window.alert("msg"); 2.确认框 window.confirm("msg"); 3.询问框 window.prompt("msg","defaulvalue") 页面 1.打开一个窗口 window.open() 2.在子窗口中使用,表示父窗口的w

  • 简单学习JavaScript中的for语句循环结构

    可以直接看示例,用得太多了,很简单 (function() { for(var i=0, len=demoArr.length; i<len; i++) { if (i == 2) { // return; // 函数执行被终止 // break; // 循环被终止 continue; // 循环被跳过 }; console.log('demo1Arr['+ i +']:' + demo1Arr[i]); } })(); 关于for循环,有一下几点需要注意 for循环中的i在循环结束之后任然存在

  • 深入学习JavaScript中的Rest参数和参数默认值

    本文将讨论使 JavaScript 函数更有表现力的两个特性:Rest 参数和参数默认值. Rest 参数 通常,我们需要创建一个可变参数的函数,可变参数是指函数可以接受任意数量的参数.例如,String.prototype.concat 可以接受任何数量的字符串作为参数.使用 Rest 参数,ES6 为我们提供一种新的方式来创建可变参数的函数. 我们来实现一个示例函数 containsAll,用于检查一个字符串中是否包含某些子字符串.例如,containsAll("banana",

随机推荐