Skip to content

JavaScript Promise剖析 #14

@luck2011

Description

@luck2011

JavaScript Promise剖析

译者大苹果注:代码中的原作者的注释使用双斜杠 //,译者解读代码的注释使用 /**/

原文地址:http://mattgreer.org/articles/promises-in-wicked-detail/

我在写JavaScript代码时使用Promises已经有一段时间了。刚开始的时候是有些摸不着头脑。现在使用起来已经得心应手了,但是当静下心来去仔细研究它的时候,我却不知道它到底是如何工作的。这篇文章就是为了理清这个问题而写的。如果能坚持读到最后,你也能对Promises的来龙去脉有个很深的了解。

我们一起来共同创建一个Promise的具体实现方案,这个实现方案符合Promise/A+规范,从中我们又能充分理解promise是怎么满足异步编程需求的。本文假定你已对Promises有大致的了解。如果还不了解,promisejs.org (中文翻译)可以使你快速入门。

为毛要弄这么清楚?

真正理解其工作原理能提升让你从中受益的能力,也有利于调试错误。这篇文章就是在我和同事遇到了Promises的问题捣乱后才激发我去写的。如果当时我知道我现在所掌握的东西,我们当时就不会被难住了。

简单用例

一开始,我们让事情越简单越好。我们想把下面的代码:

doSomething(function(value){
  console.log('Got a value: ' + value)
})

变成:

doSomething().then(function(value){
  console.log('Got a value:' + value)
})

要这样一改的话,我们的doSomething()也需要从

function doSomething(callback){
  var value = 42
  callback(value)
}

改成“Promise”的形式才行:

function doSomething(){
  return {                           /*返回值是包含then方法的对象字面量*/
    then: function(callback){
      var value = 42
      callback(value)
    }
  }
}

fiddle测试

不过这仅仅是对回调函数使用的语法糖。但这就是我们开始接触Promise的核心概念的一个开端:
Promises将最终返回值的思想俘获到一个对象里去。
Promise如此有意思的主要原因就是这个。一旦用这样的做法来处理最终返回值,我们就可以大展拳脚了。待会儿再说这个。

定义Promise样式

像上面那样的一个对象字面量肯定是hold不住的。下面来定义一个切实管用的Promise样式,并使其具有扩展性:

function Promise(fn){
  var callback = null                /*先置空callback做全局对象*/
  this.then = function(cb){
    callback = cb                    /*回调函数由then方法提供*/
  }

  function resolve(value){           /*由resolve方法真正调用回调*/
    callback(value)                  /*给回调传递返回值value*/
  }

  fn(resolve)                        /*把resolve方法交出去*/
}

doSomething()方法改造了来使用Promise:

function doSomething(){
  return new Promise(function(resolve){     /*接住resolve后执行*/
    var value = 42
    resolve(value)
  })
}

这里有个问题,当执行时跟踪调用堆栈就能发现,resolve()方法在then()方法之前被执行了,此时回调函数还是null。这个问题先用setTimeout这个hack方式来暂时解决下:

function Promise(fn){
  var callback = null
  this.then = function(cb){
    callback = cb
  }
  function resolve(value){
    // 强制回调函数在下次事件循环里才被调用,让then方法有机会先设置正确的callback
    setTimeout(function(){
      callback(value)
    }, 1)
    /*
     setTimeout可以使函数语句在给定时间后被执行,
     但是都是在下次事件循环到它了才能真正执行,
     即使0毫秒也如此, Try:
    setTimeout(function(){
      alert(1)
    }, 0)
    alert(2)
    */
  }

  fn(resolve)
}

fiddle测试
有了hack写法,我们的代码看起来像是是能用了。

实在是弱爆了

要这样写的话,Promise必须依赖异步机制才行。但这很容易就出bug,只要在执行then()方法的时候也使用异步方式,Promise里的回调函数就成null了。为什么这么快就让你大跌眼镜?因为上面的写法实在是太容易让你产生“Promise不过如此”的幻觉了。then()resolve()是不会离开的,他们是Promise的一对好基友。

Promises应具有状态

上面写法意外地揭露了,Promises应具有状态。我们在继续操作之前得搞清楚它到底处于何种状态,并确保能在它的状态下正确地继续前进。这样代码才健壮。

  • Promise要么是在挂起状态等待返回值,要么是在完成状态并持有返回值
  • 一旦Promise完成并持有返回值后,它应保持此返回值不变,并且不能被再次完成。

(Promise也能被拒绝,后面会进行错误处理)

那现在就明确的将状态引进来,这样就能将刚才的hack写法删掉:

function Promise(fn){
  var state = 'pending'  /*声明初始状态为挂起*/
  var value              /*返回值全局变量*/
  var defferred          /*用来保存回调函数*/

  function resolve(newValue){
    value = newValue     /*保存返回值*/
    state = 'resolved'   /*改变状态为完成*/
    if(defferred){       /*如果有回调函数就调用*/
      handle(defferred)
    }
  }

  function handle(onResolved){
    if(state === 'pending'){ /*如果状态还是挂起*/
      defferred = onResolved /*则仅保存回调函数后返回*/
      return
    }
    onResolved(value)        /*否则就调用回调并传递返回值*/
  }

  this.then = function(onResolved){ 
    handle(onResolved)       /*then方法负责接收回调并传递给handle方法*/
  }

  fn(resolve)                /*开始就将resolve方法交出去*/
}

fiddele测试
代码变得复杂了些,但是现在可以随时去调用then()方法和resolve()方法,异步和同步两种编程方式都管用。

这就是state状态的功劳。then()resolve()方法都交给中间人handle()方法去处理,它会根据具体情况去走下一步:

  • 如果在执行resolve()方法前执行then()方法:意味着此时还没有返回值,这时状态还处于挂起状态,因此要先保存then传递的回调函数。等到resolve()方法被执行后,返回值已经到位,就可以执行刚保存的回调并传递返回值给它了。
  • 如果在执行then()方法前执行resolve():意味着现在已只持有返回值。当then()方法执行后就能拿到它传递的回调函数,就可以执行回调并传递返回值了。

注意到setTimeout已经不见了吗?都是暂时的,它还会回来的,还是先挨个说吧。

有了Promises,我们与它打交道的顺序就不重要了,可以在合适的时机去随心所欲的执行then()resolve()。这就是上面说的将最终返回值的思想俘获到一个对象里去的强悍的优势。

我们仍然有许多东西要添加,但是现在的Promise已经足够强大。它已经支持将then()方法执行任意多次了,每次都能获得同一个返回值:

var promise = doSomething()

promise.then(function(value){
  console.log('Got a value:', value)
})

promise.then(function(value){
  console.log('Got the same value again:', value)
})

本文所实现的Promise方法并不完全正确!反例就是:在resolve()方法执行前,真的执行了若干次then()方法,则只有最后一次传递的回调才会被真正执行。解决方案就是不能使用一个defferred变量保存回调函数,而是一个队列。我决定不这么做是为了让本文足够简短,它现在已经够长的了。:)

(待续)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions