2

本文为饥人谷讲师若愚原创文章,首发于 前端学习指南

问题

假设我们有一个需求:1. 获取用户所在的城市;2. 根据城市获取天气;3. 根据天气获取出行建议。那我们的代码应该是这样的

getCity(url1, function(){
  getWeather(url2, function(weather){
    getSuggestion(url3, function(suggestion){
      console.log(suggestion)
    })
  })
})

这就是典型的异步 callback 『回调地狱』,代码层层嵌套可读性很差。关于异步的解决方式可参考这篇文章 Node.js异步漫谈

使用 Promise 是解决上述问题的一种方式,这里我们不去讲如何去使用内置的 Promise,而是带大家手把手写一个 Promise。

思路

我们希望有一个工具,能让我们使用下面的的写法来实现上述功能

promise.then(getCity)
    .then(getWeather)
    .then(getSuggestion)

整理下思路:

  1. Tool 是一个对象
  2. Tool 有 then 这个方法
  3. 执行 then 方法返回的应该还是 Tool 对象
function Promise(){}
Promise.prototype.then = function(fn){
  //todo...
  return this
}
var promise = new Promise()

那如何实现异步操作序列执行呢?关键思路如下:

在 promise 对象内容维护一个数组,当执行 promise.then(getCity) .then(getWeather) .then(getSuggestion) 时把这几个函数依次放入数组中。注意此时这些函数并没有执行。

执行promise.resolve()时,会从数组中拿出一个函数去执行。函数执行的过程中在异步操作的结果到来后会再次自动调用 promise.resolve(),触发下一个函数的取出并执行,下一个函数结果到来后再次自动调用promise.resolve() ......,这样就实现了异步链式执行。和原子弹爆炸原理类似。

所以需要对原来的异步函数做一点小小的改动,在数据到来的地方,加一个promise.resolve,用于启动后续函数的执行

function getCity(){
  var xhr = new XMLHttpRequest()
  xhr.open(url, 'get', true)
  xhr.onload = function(){
    if (this.status == 200) {
      promise.resolve(xhr.responseText)  //注意这里的promise.resolve
    }
  }
  xhr.send()
}

现在我们就能实现一个简易的 Promise 了,这里我们先暂不考虑特殊情况:

function Promise(){
  this.callbacks = []
}
Promise.prototype.then = function(fn){
  this.callbacks.push(fn)  //调用 then 时把函数放入数组
  return this              //返回当前对象供链式调用
}
Promise.prototype.resolve = function(data){
  var fn = this.callbacks.shift()  //当调用resolve时拿出一个函数
  fn&&fn(data)                     //执行这个函数,并且把resolve的参数做参数
}


var promise = new Promise()

promise.then(getCity)
    .then(getWeather)
    .then(getSuggestion)

promise.resolve()  //启动

function getCity(){
  setTimeout(function(){
    promise.resolve('杭州')
  }, 1000)
}
function getWeather(city){
  setTimeout(function(){
    promise.resolve(city + ' 晴天')
  }, 1000)
}
function getSuggestion(weather){
  setTimeout(function(){
    console.log(weather + ' 天气不错,可携女友与狗出行')
  }, 1000)
}

当然,如果觉得promise.resolve 单独启动一次看起来不舒服,也可以这样执行

getCity()
  .then(getWeather)
  .then(getSuggestion)

function getCity(){
  setTimeout(function(){
    promise.resolve('杭州')
  }, 1000)
  return promise   //注意这里
}

实现

到此为止我们已经写了一个简单的 Promise,甚至能满足很大一部分使用需求。但有个问题,每次异步操作可能存在失败的情况,而上面的代码并没有异步函数的失败处理。下面考虑异步的失败处理,原理和上面类似,可以阅读代码动手做个测试

class Promise {
    constructor (){
      this.callbacks = []
      this.oncatch = null
    }

    reject(result){
      this.complete('reject', result)
    }

    resolve(result){
      this.complete('resolve', result)
    }

    complete(type, result){
      if(type==='reject' && this.oncatch){
        this.callbacks = []
        this.oncatch(result)
      }else if(this.callbacks[0]) { 
        var handlerObj = this.callbacks.shift()
        if(handlerObj[type]){
          handlerObj[type](result)
        }
      }
    }

    then(onsuccess, onfail){
      this.callbacks.push({
        resolve: onsuccess,
        reject: onfail
      })
      return this
    }

    catch(onfail){
      this.oncatch = onfail
      return this
    }
  }

  var promise = new Promise()
  fn1().then(fn2, onfn1error)
       .then(fn3, onfn2error)
       .catch(onerror)

  function fn1(){
    setTimeout(function(){
      if(Math.random()>0.5){
        promise.resolve('杭州')
      }else{
        promise.reject('fn1 error')
      }
    })
    return promise
  }

总结

现在我们已经手写了一个 Promise, 当然和浏览器内置对象Promise原理有些差异, 但至少『达到』类似的目的了

加微信号: astak10或者长按识别下方二维码进入前端技术交流群 ,暗号:写代码啦

每日一题,每周资源推荐,精彩博客推荐,工作、笔试、面试经验交流解答,免费直播课,群友轻分享... ,数不尽的福利免费送


饥人谷
1k 声望131 粉丝

最有爱的前端交流社区