十行代码实现高仿Promise

silvergoose 发布于7天前 阅读19次
0 条评论

文章作者:若愚@饥人谷,首发于 前端学习指南 - 知乎专栏 。 转载需在文章显要位置声明来源

问题

假设我们有一个需求: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')
      }
    })
  }

总结

现在我们已经手写了一个 Promise,理解 Promise 的原理并熟练使用应该不再话下了吧,加油~

加入大前端QQ学习群9群:542597149, 更多干货等你来拿

查看原文: 十行代码实现高仿Promise

共收到0条回复

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。