这是我参与更文挑战的第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的方法
一个没有超时的案例
其实大部分我们使用的方法都能够在异步的回调函数中或者Promise
的finally
中得到结果,在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()都会有结果(解决或拒绝),至此我们也很容易想到怎么来解决这个问题了。
- 将拿取token的方法改成Promise对象,并另外需要一个超时任务的Promise对象
- 将两个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捕获从而进行异常处理。