ES6 Promise对象

javascript/jquery

浏览数:728

2017-5-10

ES6中,新增了Promise对象,它主要用于处理异步回调代码,让代码不至于陷入回调嵌套的死路中。

Promise本质

Promise本质上是一个 函数 ,更确切地说,它是一个 构造器 ,专门用来构造对象的。
它接受一个函数作为参数,并返回一个对象,大致情况如下:

 function Promise( fn ){
    // var this = {}
    // Object.setPrototypeOf(this, Promise.prototype)
    // 接下来,是Promise函数具体要实现的功能,这部分由系统帮我们完成
    ...  
    // 最后返回这个Promise实例
    return this
  }

Promise函数的参数,作为函数形式存在,需要我们手动去编写。
它需要两个参数,情况如下:

function fn(resolve, reject){
    ...  // 我们自己写的逻辑代码
}

Promise函数的返回值是一个对象,准确来说,是Promise自己生成的实例。
其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。
该实例有三种状态,分别是: 进行 状态、 完成 状态 和 失败 状态。
该实例只能从“ 进行 状态”转变为“ 完成 状态”,或从“ 进行 状态”转变为“ 失败 状态”,这个过程不可逆转,也不可能存在其他可能。因为Promise就是用来管理业务状态的一种机制,它能够保证业务的顺序执行,而不出现混乱。

这就好比我们在家里炒一份菜,是只可能存在“ 正在炒菜 ”、“ 炒好了 ”和“ 炒糊了 ”这三个阶段的,而“正在炒菜”的状态肯定是会优先存在于“炒好了”和“炒糊了”两个状态前面,“炒好了”和“炒糊了”本身又是两个 互斥的事件 ,所以这个过程,只可能出现从“正在炒菜”状态过渡到“炒好了”或者“炒糊了”状态的情况,永远不可能从“炒好了”过渡到“炒糊了”状态,也不可能从“炒糊了”过渡到“炒好了”状态。

那么,这些由Promise函数构建出来的对象,究竟有着什么用处呢?
我们先来看一组代码:

fn( ( ( ( ()=>{} )=>{} )=>{} )=>{} )

像这样回调之中调回调的情况,在Node开发中,是一件很常见的事。
Node本身是一个无阻塞、无空耗、并发、依赖于系统底层读写事件的运行环境,它的回调机制保证了它在异步并发执行过程中回调链的独立性和抗干扰能力,但同时也带来了很大的副作用,最大的麻烦就是,采用普通回调方式书写出来的Node回调代码十分混乱。

其实,面向过程或面向对象的函数式编程,本身就是一个巨大的“函数调用”过程。我们在代码中使用函数,并在函数中调用函数,运行环境帮助我们维护一个或多个函数栈,以实现程序的有序执行,及增强软件后期维护的便利性。

但如果我们能把这种不断调用的过程给摊开成 平面 ,而不要使函数相互嵌套,就会使我们的软件可维护性提升很大一个台阶。我们只需要将原本写好的功能一个个罗列出来,并构造出一根供函数调用的链条,把这些功能一个个地按需调用,软件的功能不就实现了么?而且还更清晰明了。
Promise帮助我们将函数摊开来,形成一根调用链条,让程序有序执行。

每一个返回值为Promise实例的函数,都是 Promise调用链条上的一个结点 ,这个Promise实例维护着该处函数的运行状态,并决定着自身的生存周期。它的写法大致是这样的:

// 执行一个返回值为promise的函数 并通过resolve或reject返回
  promiseFn_1(...)
  // 将多个返回值为promise的函数合成一个 并通过resolve或reject返回
  // Promise.all( promiseFn_all_1, promiseFn_all_2, ... )
  // Promise.race( promiseFn_race_1, promiseFn_race_2, ... )
  //
  .then(
    (...resolveArgs)=>{ ... promiseFn_resolve_1(...) ... },
    (...rejectArgs)=>{ ... promiseFn_reject_1(...) ... },
  )
  .then(
    (...resolveArgs)=>{ ... promiseFn3_resolve_2(...) ... },
    (...rejectArgs)=>{ ... promiseFn3_reject_2(...) ... },
  )
  ...
  .catch(
    (...rejectArgs)=>{ ... promiseFn_catch_1(...) ... }
  )
  ...
  .finally(
    (...simpleArgs)=>{ ... }
  )

上面的代码看似及其繁琐,其实结构层次已经比使用普通回调方式书写的代码好很多了(虽然还是显得有些混乱)。
当我们了解了Promise中这些函数(如then()、catch()、finally())的具体意思,就会明白它的具体意思了。
接下来我们就来构建一个Promise实例,看一看这根“链条”上的结点(也就是上面以“promiseFn_”开头的函数)到底长什么样。

function promiseFn_1(path, options){
    return new Promise((resolve,reject)=>{
      // 需要执行的具体代码,一般情况下,是调用一个带有回调参数的函数
      // 此处使用fs模块中的readFile函数作为示例
      fs.readFile(path, options, (err,data)=>{
        if(err){
          reject(err)
          // 这样使用可能会更好:
          // throw new Error(path+' :  文件读取出现未知的错误!')
        }
        resolve(data)
      })
    })
  }

上面Promise参数函数中,出现了两个陌生的参数,resolve和reject。它们其实是在Promise运行完成后,主动向该回调函数中传入的参数。这个过程,由Promise函数自动帮我们完成。
resolve和reject都是与Promise实例相关的函数,用于改变Promise实例的状态。
resolve函数能使Promise实例从“进行”状态变成“完成”状态,并将自己接受到的参数传给下一个promise对象。
reject函数能使Promise实例从“进行”状态变成“失败”状态,并将自己接受到的参数传给下一个promise对象(一般是一个错误对象)。

Promise的几个重要方法

promise Promise.prototype.then( resolveFn, [rejectFn] )

@param resolveFn( ...args )  
    函数,当Promise实例状态变为“完成”状态时会被执行,  
    用于将从当前promise中取出reresolve( ...args )中得到的参数(...args),  
    并进行相应的操作,比如将(args)传入另一个封装了promise构造器的函数,  
    并将该函数执行完成后返回的promise实例返回  
    @param ...args  
      参数列表,当前promise实例处于“完成”状态时,通过resolve(...args)得到的值。  
  @param [rejectFn( ...args )]  
    函数,可选,当Promise实例状态变为“失败”状态时会被执行,  
    用于将从当前promise中取出reject( ...args )中得到的参数(...args),  
    并进行相应的操作,比如将(args)传入另一个封装了promise构造器的函数,  
    并将该函数执行完成后返回的promise实例返回  
    @param ...args  
      参数列表,当前promise处于“完成”状态时,通过resolve(...args)得到的值。  
  @return promise  
    promise对象,resolveFn或rejectFn执行后的返回值,  
    我们一般会在fn中调用另一个封装了promise构造器的函数,  
    然后将其返回给then()方法,then()方法再将其作为then的返回值返回给当前链式调用处,  
    如果fn()返回的不是一个promise对象,then()会帮我们将fn()返回值封装成promise对象,  
    这样,我们就可以确保能够链式调用then()方法,并取得当前promise中获得的函数运行结果。  

hen()方法定义在Promise.prototype上,用于为Promise实例添加状态更改时的回调函数,相当于监听一样。
当当前promise实例状态变为“完成”状态时,resolveFn函数自动执行。
当当前promise实例状态变为“失败”状态时,rejectFn函数自动执行。

promise Promise.prototype.catch( rejectFn )

@param rejectFn( ...args )  
    函数,当Promise实例状态变为“失败”状态时会被执行,  
    用于将从当前promise中取出reject( ...args )中得到的参数(...args),  
    并进行相应的操作,比如将(args)传入另一个封装了promise构造器的函数,  
    并将该函数执行完成后返回的promise实例返回  
    @param ...args  
      参数列表,当前promise处于“完成”状态时,通过resolve(...args)得到的值。  
  @return promise  
    promise对象,rejectFn执行后的返回值,  
    如果fn()返回的不是一个promise对象,catch()会帮我们将fn()返回值封装成promise对象,  
    并将其返回,以确保promise能够被继续链式调用下去。  

该方法其实是“.then(null, rejectFn)”的别名,用于指定状态转为“失败”时的回调函数。
建议不要在then()方法中定义第二个参数,而应该使用catch(),结构层次会更好一些。
如果没有使用catch()方法指定错误错误处理的回调函数,promise实例抛出的错误不会传递到外层代码。
如果promise状态已经变为了resolved(“失败”状态),再抛出任何错误,都是无效的。
promise实例中抛出的错误具有冒泡的特性,它会一直向后传递,直到被捕获为止。

Promise.all( [promise1, promise2, …, promisen] )

@param [promise1, promise2, ..., promisen]
    可遍历对象,一个由promise对象构成的可遍历对象,常用数组表示
  @return promise
    promise对象  

Promise.all()用于将多个Promise实例包装成一个新的Promise实例,并返回。
Promise.all()方法接受一个由Promise实例组成的可遍历对象。如果可遍历对象中存在有不是Promise实例的元素,就会调用Promise.resolve()方法,将其转为Promise实例。
本文的可遍历对象,指的是那些具有Iterator接口的对象,如Array、WeakSet、Map、Set、WeakMap等函数的实例。
Promise.all()方法返回的Promise实例的状态分成两种情况:

  • 可遍历对象中的Promise实例状态全变为 完成 状态时,该实例的状态才会转变为 完成 状态,此时,可遍历对象中的Promise实例的返回值会组成一个数组,传给该实例的回调。
  • 可遍历对象只要存在Promise实例状态转为 失败 状态时,该实例的状态就会转变为 失败 状态,此时,第一个转为 失败 状态的Promise实例的返回值会传给该实例的回调。

Promise.race( [promise1, promise2, …, promisen] )

@param [promise1, promise2, ..., promisen]
    可遍历对象,一个由promise对象构成的可遍历对象,常用数组表示
  @return promise
    promise对象 

Promise.race()与Promise.all()用法基本上一致,功能上也几乎相同,唯一的差异就是:
Promise.race()方法返回的Promise实例的状态分成两种情况:

  • 可遍历对象只要存在Promise实例状态转为 完成 状态时,该实例的状态才会转变为 完成 状态,此时,第一个转为 完成 状态的Promise实例的返回值,会作为该实例的then()方法的回调函数的参数。
  • 可遍历对象只要存在Promise实例状态转为 失败 状态时,该实例的状态就会转变为 失败 状态,此时,第一个转为 失败 状态的Promise实例的返回值,会作为该实例的then()方法的回调函数的参数。

promise Promise.resolve( notHaveThenMethodObject )

@param notHaveThenMethodObject
    对象,一个原型链上不具有then()方法的对象
  @return promise
    promise对象

如果Promise.resolve()的参数的原型链上不具有then方法,则返回一个新的Promise实例,且其状态为 完成 状态,并且会将它的参数作为该实例的then()方法的回调函数的参数。
如果Promise.resolve()的参数是一个Promise实例(原型链上具有then方法),则将其原封不动地返回。
Promise.resolve()方法允许调用时不使用任何参数。

promise Promise.reject( something )

@param something
    任意值,用于传递给返回值的then()方法的回调函数参数的值
  @return promise
    promise对象

Promise.reject方法的用法和resolve方法基本一样,只是它返回的Promise实例,状态都是 失败 状态。
Promise.reject方法的参数会被作为该实例的then()方法的回调函数的参数。
Promise.resolve()方法允许调用时不使用任何参数。

Promise构造器回调函数参数中的 resolve 和 reject 和Promise构造器方法中的 reject() 和 resolve() 效果是不一样的。
Promise构造器回调函数参数中的 resolve 和 reject 用于更改当前Promise的状态,并将其值返回给当前Promise的then()方法的参数。 Promise构造器方法中的 reject() 和 resolve() 可以直接返回一个已经改变状态的新的Promise对象。

  • Promise.reject() Promise.resolve()
  • new Promise((resolve, reject)=>{ resolve(…) 或 reject(…) })

Promise.prototype.done( [resolveFn], [rejectFn] )

@param [resolveFn( ...args )]  
    函数,可选,当Promise实例状态变为“完成”状态时会被执行,  
    用于将从当前promise中取出reresolve( ...args )中得到的参数(...args),  
    并进行相应的操作,比如将(args)传入另一个封装了promise构造器的函数,  
    并将该函数执行完成后返回的promise实例返回  
    @param ...args  
      参数列表,当前promise实例处于“完成”状态时,通过resolve(...args)得到的值。  
  @param [rejectFn( ...args )]  
    函数,可选,当Promise实例状态变为“失败”状态时会被执行,  
    用于将从当前promise中取出reject( ...args )中得到的参数(...args),  
    并进行相应的操作,比如将(args)传入另一个封装了promise构造器的函数,  
    并将该函数执行完成后返回的promise实例返回  
    @param ...args  
      参数列表,当前promise处于“完成”状态时,通过resolve(...args)得到的值。  

不管以then()或catch()方法结尾,若最后一个方法抛出错误,则在内部可能无法捕捉到该错误,外界也无法获得,为了避免这种情况发生,Promise构造器的原型链上提供了done()方法。
promise.done()方法总是处于会调链的低端,它可以捕捉到任何在回调链上抛出的错误,并将其抛出。

Promise.prototype.finally( simpleFn )

 @param simpleFn  
    一个普通函数,这个普通函数无论如何都会被执行。  

finally方法指定,不管Promise对象最后状态如何,都会执行的操作。

代码参考

finally()的实现

Promise.prototype.finally = function( simpleFn ){
    let Pro = this.constructor
    return this.then(
      value => Pro.resolve( simpleFn() ).then( () => value ),
      error => Pro.resolve( simpleFn() ).then( () => { throw error } )
    )
  }

done()的实现

 Promise.prototype.done = function( resolveFn, rejectFn ){
    this
      .then( resolveFn, rejectFn )
      .catch( error => {
        // 这是一个把需要执行的代码,从任务队列中拉出来的技巧
        setTimeout( () => { throw error }, 0)
      } )
  }

这儿使用了一个很常用的技巧:
我们来看一下这个例子:

 for(let i of [1,2,3]){
    setTimeout( () => { console.log( 'setTimeout ' + i ) }, 0)
    console.log( 'console ' + i )
  }

最终结果是:

  > console 1  
  > console 2  
  > console 3  
  > undefined  
  > setTimeout 1  
  > setTimeout 2  
  > setTimeout 3  

javascript除了维护着当前任务队列,还维护着一个setTimeout队列。所有未被执行的setTimeout任务,会按顺序放到setTimeout队列中,等待普通任务队列中的任务执行完,才开始按顺序执行积累在setTimeout中的任务。
简而言之, javascript会在执行完当前任务队列中的任务后,再执行setTimeout队列中的任务 。
我们设置任务在0s后执行,可以将该任务调到setTimeout队列中,延迟该任务发生,使之异步执行。
这是异步执行方案当中,最常用,也最省时省事的一种方式。

加载图片

function preloadImage(path){
    return new Promise( (resolve, reject) => {
      let img = document.createElement('img')
      img.style.display = 'none'
      document.body.appendChild(img)
      // 当图片加载完成后,promise转为完成状态
      // 此时,我们可以把该节点的图片加载在应有的地方,并且将其删除
      img.addEventListener('load', resolve)
      // 当图片加载出错后,promise转为失败状态
      img.addEventListener('error', reject)
      img.src = path
    } )
  }  

Generator与Promise联合

// Promise的包装函数 getFoo()
  function getFoo(){
    // ......something
    return new Promise( (resolve, reject) => {
      // ......something
      resolve('foo')
    } )
  }
  // Generator函数 generator()
  function* generator(){
    try{
      let foo = yield getFoo()
      console.log(foo)
    }
    catch(error){
      console.log(error)
    }
  }
  // 自动执行generator函数的函数,现在可以用async语法替代它
  function run(generator){
    // 让generator函数运行至第一个yield语句前,
    // 并获得getFoo()的结果---一个promise函数
    let it = generator()
    function go(result){
      if(result.done) return result.value
      return result.value.then( value => {
          // 利用尾递归来实现自动执行,让本次递归产生的栈单元项只有一个
          return go( it.next(value) )
        }, error => {
          return go( it.throw(error) )
        }
      )
    }
    go(it.next())
  }
  // 调用run方法
  run(generator)

http://blog.freeedit.cn/2017/04/24/ES-0004-ES6-Promise-Object/#3-代码参考