精读JS系列(9b) Promise — 回调地狱、Promise构造器

本文将简单的介绍一下Promise以及promisify的方法,不涉及底层源码细则,最多从 Promise A+ 规范角度进行解读。

Yoshi, 开始吧。

回调地狱

在认识Promise之前,有必要知道什么是 回调地狱(Callback Hell),它还有一个显得很霸气却极为令所有JS开发者感到无比厌烦的名字—— 厄运金字塔(Pyramid of doom)。之所以先了解这些,这是因为Promise本来就是为了避免在多重异步操作时产生深层次的嵌套回调提出的一种新型异步解决方案。

老样子,来个栗子

  function load(src,cb){
      let es = document.createElement('script')
      es.src = src 
      document.body.append(es)
      es.onload=()=>{cb()}
      es.onerror=()=>{cb(new Error('failed to load '+src ))  }
  }
  load('./t1.js', err=>{
      if(err===undefined) {
          f1();      // t1内部的函数
      } else {
          console.log(err.message);
      }
  })

上面的代码是很很常见的需求,随便添加一个js,然后执行里面的内容。 但是也会需要加载多个脚本,同时执行一些函数:

 load('./t1.js', err=>{
     if(err===undefined) {
         f1();      // t1内部的函数
         load('./t2.js', err=>{
             if(err===undefined) {
                 f2();      // t2内部的函数
                 load('./t3.js', err=>{
                     if(err===undefined) {
                         f3();      // t3内部的函数
                     } else {
                         console.log(err.message);
                     }
                 })
             } else {
                 console.log(err.message);
             }
         })
     } else {
         console.log(err.message);
     }
 })

这样的多重操作,会随着嵌套层数变深逐渐失控,于是便形成了臭名昭著的厄运金字塔。为了解决这个问题,于是Promise便应运而生——主要是将深层回调变成了链式调用,这样能更符合人类逻辑:

第一步:先让load返回一个Promise

  // 每个 load 返回一个 Promise 对象
  // 每个Promise对象都有一个 then 方法。
  function load(src){
      let es = document.createElement('script')
      es.src = src 
      document.body.append(es)
      return new Promise((resolve, reject)=>{
          es.onload=()=>{resolve('success '+ src)}
          es.onerror=()=>{reject(new Error('failed to load '+src ))  }
      })
  }

第二步: 链式调用:

 load('./t1.js')
     .then(str=>{
         console.log(str);
         f1();
         return load('./t2.js')   //这里返回一个Promise
     })                           // 直接就能用 then
     .then(str=>{
         console.log(str);
         f2();
         return load('./t3.js')		// 再返回一个Promise
     })
     .then(str=>{           
         console.log(str);
         f3();
     })
     .catch(er=>{
         console.log(er.message);
     })

是不是有一种……之后的舒爽感??

Promise极其重要的特征是链式调用,每个load函数返回一个Promise,该Promise确定是异步加载成功还是失败之后就会被返回然后提供给下一个操作使用。 一个Promise队列就像是使用同步代码来实现异步操作一样。


认识Promise

这里就引用 MDN 上的Promise概念:

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

重要说明

  • Javascript中的Promise也参考了Promise/A+规范。
  • Promise中的概念还是 ** MDN** 比较全面,因此我会根据 MDNPromise/A+ 的内容综合说明。
  • 嘛,其实Promise概念不如何重要,重要的是如何应用它

正文开始。

Promise构造:

语法:

var p promise = new Promise(function(resolve, reject){ // executor })

参数:

  • executor : 在新建一个Promise实例时,必须向 Promise构造函数 传递一个回调函数(名曰:executor),在新建对象时会立即执行它。 返回值:
  • 一个被设置了状态的Promise,它可能会有valuereason。 亦或是该Promisepending

附注 I:关于executorresolvereject

  1. 它必须有两个回调函数:resolvereject,无论是resolve还是 reject,这两个都是Promise内部提供的。
  2. 首次执行resolve时,Promise状态就已经被固定fulfilled ,并且会有一个固定value
  3. 首次执行reject时,Promise状态就被固定rejected ; 并且会有一个固定reason
  4. 一旦Promise确定, 之后不论是再执行resolvereject亦或是抛出异常,都将被忽略。换言之,当Promise被确定为fulfilled后,就无法再转换到rejected,反之亦然。
  • 既没有执行resolve也没有reject或抛出异常,那么Promise状态为pending,此时它可能会被转换到fulfilledrejected

附注 II: 关于settled

  • 一旦Promise被确定为fulfilledrejected, 都可以说Promisesettled
  • 一个未被settledPromise将处于pending状态。

注意: 现在最好是使用Firefox浏览器进行实验,因为Chromeconsole做了优化,结果可能和理论不一致

综上所述,可以知道:

(一) Promise状态一经确定(settled),就不会再二次改变:
  let pr = new Promise((res,rej)=>{
      res('im fulfilled')
      console.log(11111);
      
      res('im fullfiled two')
      console.log(22222);
      
      rej(new Error('im rejected'))
      throw new Error('hei, im exception')
  })
  console.log('33333');
  
  console.log(pr);

浏览器输出:

务必注意:

  1. 111112222233333是立即输出的,因为executor是立即执行的。
  2. 因为executor没有异步操作, 所以Promise会立即确定状态。
  3. 一个executor可以多次调用resolvereject,只是第一次调用才会生效,其他全被忽略
  let pr = new Promise((res,rej)=>{
      rej('im rejected')
      console.log(11111);
      res('im fullfiled two')	// 被忽略!!
      console.log(22222);		// 正常执行
   })
   pr.then(undefined,er=>{})    // 如果注释掉,它会抛出一个异常!!!
  console.log('33333');
  
  console.log(pr);

浏览器输出:

(二) Promise如果是异步确定状态的,那么在此之前处于pending
 let pr = new Promise((res,rej)=>{
     console.log(11111);
     setTimeout(res,1000,'ha,ha,ha,ha');
     console.log(22222);
     setTimeout(res,500, 'en,en,en,en');  // 先执行,
 })
 console.log(3333);
 console.log(pr);		// pending
 setTimeout(console.log, 1500, pr)  // fulfilled

浏览器输出:

根据以上推论,如果Promise不执行resolve时,那么就永远无法被settled,即会一直处于pending状态。

(三) Promise是无法手动取消的

一个promise返回后由GC回收,并且在JS中没有提供关于对象的销毁方法,因此它会一直存在当当前宏任务结束。


最后

…… 知识点不够集中,太多概念就会堆积在一起,很容易混乱。 所以决定——把之后的内容放到下一篇,这样读取来能容易些。嘛,为了自己之后方便整理。

作者:Lazy_K
链接:https://juejin.im/post/5ed5b708e51d452f9c27cead


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流