给自己的异步任务加一个超时功能

2,928 阅读5分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

前言

这几天我在开发一个混合开发的项目时,需要和原生进行通信。当我为了更好的使用方法,高高兴兴的将方法封装成Promise对象时,却出现了一个意想不到的问题。为了更好的理解这个问题,先将js的异步方法简单回忆一下:

异步的方法

回调函数

回调函数很好理解,将函数当作参数传递给异步的方法中,在异步任务完成后,执行该方法,达到同步的效果

function sleep(time,cb){
  setTimeout(()=>{
    cb()
  },time)
}

sleep(1000, () => {
  console.log('sleep 1s')
})
//1s后 输出sleep 1s

Promise

Promise对象是一个构造函数,用来生成Promise实例。它有三个状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 将上面回调函数的例子简单改一下就可以变成回调函数的形式

function sleep(time) {
  return new Promise((resolve, reject) => {
    //异步方法
    setTimeout(() => {
      resolve('sleep 1s')
    }, time)
  })
}
sleep(1000).then(res => {
  console.log(res)
})
//1s后 输出sleep 1s

Promise和回调函数的结合

在实际开发过程中,经常会将回调函数的形式改成Promise对象来使用,例如一些原本使用回调函数的方法,如果你爱上了promise这种形式,你完全可以将其结合起来,这里使用类似jquery的ajax请求为例子

function requestPromise(ajaxOption) {
  return new Prmise((resolve, reject) => {
    $.ajax({
      ...ajaxOption,
      success: res => {
        resolve(res)
      },
      error: err => {
        reject(err)
      }
    })
  })
}
requestPromise({
  url: 'https://api.demo.com',
  type: 'GET',
}).then(res=>{
  console.log(res)
}).catch(err=>{
  console.error(err)
})

众所周知,jq中的ajax请求是一个用回调函数来处理异步的方法。将方法被Promise对象简单包装一下,就变成了Promise的方法

一个没有超时的案例

其实大部分我们使用的方法都能够在异步的回调函数中或者Promisefinally中得到结果,在sleep的例子中的setTimeout会在延迟1s后执行回调函数,或者通过resolve返回一个fulfilled的状态。在jq的ajax中就算请求超时,也可以通过设置timeout选项来设置超时,最后会执行error的回调函数返回异常。但是在考虑场景不全的情况可能会出现不执行回调函数的情况,或者不执行resolve/reject的情况。

上接前言,在混合开发的项目中,通过WebViewJavascriptBridge,我们可以很方便的实现OC和Javascript互调的功能,从而实现通信,通信的过程毫无疑问是异步的,在WebViewJavascriptBridge中,就是使用回调函数的形式来实现。

function setupWebViewJavascriptBridge(callback) {
  if (window.WebViewJavascriptBridge) {
    return callback(WebViewJavascriptBridge)
  }
  if (window.WVJBCallbacks) {
    return window.WVJBCallbacks.push(callback)
  }
  window.WVJBCallbacks = [callback]
  var WVJBIframe = document.createElement('iframe')
  WVJBIframe.style.display = 'none'
  WVJBIframe.src = 'https://__bridge_loaded__'
  document.documentElement.appendChild(WVJBIframe)
  setTimeout(function () {
    document.documentElement.removeChild(WVJBIframe)
  }, 0)
}
setupWebViewJavascriptBridge(bridge => {
  //ios 拿取token
  bridge.callHandler('gettoken', {foo: 'bar'}, newtoken => {
    resolve(newtoken)
  })
})

以上就是一个例子:从IOS原生应用中获取登录凭证Token。而且为了更好的使用,我包装了一层promise为了能够用上ES7中的async/await语法糖(优雅的处理异步😀) 当我兴高采烈的在IOS应用的webview中打开项目时,非常成功的实现了功能。正当我觉得万无一失的时候开始提测时,测试告诉我他打开的时候出现了白屏。我心里暗暗吐槽,怎么可能,肯定是你打开姿势不对......

分析原因

怎么会白屏呢,我并没有出现这个情况呢,我用着鄙夷的眼光走向测试并了解复现的方法。😂😂。原来是因为这个项目除了在我们的app中使用,还要让客户使用其他浏览器中打开,因为部分功能在不登陆的情况下也是可以使用。当我了解到复现场景后,立刻想到肯定是获取Token的时候出的错,但当我打开控制台时却并没有报错,在我无数次debugge后终于发现,代码根本就没有进入回调函数中。

原来只有在app中需要将js注入,才能调用相关的方法,在其他应用中(浏览器,微信...等)打开页面因为没有注入方法,也无从调用,最后代码将无法执行到回调函数,我封装的Pormise的对象中也不会执行resolve方法。最终await因为一直没有返回而堵塞住,导致最终的白屏

解决办法-加上超时处理

回调函数中的超时处理

在回调函数中其实很容易想到,只要在异步任务执行的同时,加上一个定时任务。异步任务和定时任务会同时执行,谁先执行完成,谁执行回调函数就好了,如果定时任务完成之后,异步任务还没有返回结果,那么自然是超时了

function timeourFn(cb,timeout){
  //异步任务
  async((res)=>{
    cb(res)
  })
  //超时任务
  setTimeout(() => {
    cb(new Error('timeout'))
  }, timeout);
}

timeourFn((res)=>{
  if(res instanceof Error){
    //5s后 超时了
  }
},5*1000)

Promise中的超时处理

但是很遗憾,WebViewJavascriptBridge的方法不是我们自己的实现,所以好像用回调函数无法解决了。但是我们可以用Promise中的race方法来实现。Promise.race()方法是将多个 Promise 实例,包装成一个新的 Promise 实例,和字面意思一样,“竞赛”就是这个方法的实质。只要其中一个Promise率先执行完(不论是解决或拒绝),Promise.race()都会有结果(解决或拒绝),至此我们也很容易想到怎么来解决这个问题了。

  1. 将拿取token的方法改成Promise对象,并另外需要一个超时任务的Promise对象
  2. 将两个Promise对象放在数组中,在race方法中执行。
function getToken(timeout){
  return Promise.race([
    new Promise((resolve, reject) => {
        setupWebViewJavascriptBridge(bridge => {
          bridge.callHandler('gettoken', {foo: 'bar'}, newtoken => {
            resolve(newtoken)
          })
        })
    }),
    //超时任务
    new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(new Error('timeout'))
      }, timeout)
    })
  ])
}

当timeout毫秒后,gettoken没有返回token,race方法的参数数组中的第二个Promise对象将会执行完成,执行reject,抛出超时异常。至此,业务代码中执行getToken将不会再阻塞住之后的代码,而会被catch捕获从而进行异常处理。