This is the first day of my participation in Gwen Challenge

preface

These days I need to communicate with native while working on a mixed-development project. When I happily wrapped the method as a Promise object for better use, I had an unexpected problem. To better understand this problem, let’s take a quick look at javascript’s asynchronous methods:

Asynchronous method

The callback function

The callback function is well understood, passing the function as an argument to an asynchronous method and executing the method after the asynchronous task is complete to achieve the synchronous effect

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

sleep(1000.() = > {
  console.log('sleep 1s')})// Output sleep 1s after 1s
Copy the code

Promise

The Promise object is a constructor that generates a Promise instance. This is a big pity. It has three states: Pending, fulfilled and Rejected. Only the result of an asynchronous operation can determine the current state, and no other operation can change the state. A simple modification of the above example of a callback function can be used as a callback function

function sleep(time) {
  return new Promise((resolve, reject) = > {
    // Asynchronous method
    setTimeout(() = > {
      resolve('sleep 1s')
    }, time)
  })
}
sleep(1000).then(res= > {
  console.log(res)
})
// Output sleep 1s after 1s
Copy the code

A combination of Promise and the callback function

In practice, callback functions are often used as Promise objects. For example, some methods use callback functions. If you fall in love with the Promise form, you can combine them

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)
})
Copy the code

As you know, Ajax requests in JQ are a way to handle asynchrony using callback functions. Simply wrapping a method around a Promise object makes it a Promise method

A case with no timeouts

In fact, most of the methods we use can get results in asynchronous callback functions or Promise finally. In the example of sleep, setTimeout will execute the callback function after a delay of 1s, or resolve will return a depressing state. In JQ ajax, even if the request times out, it can be set to timeout by setting the timeout option, and the error callback function will be executed to return an exception. However, it is possible to not execute the callback function, or resolve/reject, when considering scenarios that are incomplete.

In the above introduction, in the mixed development project, we can easily realize the function of OC and Javascript intermodulation through WebViewJavascriptBridge, so as to realize communication. The communication process is undoubtedly asynchronous. In WebViewJavascriptBridge, This is implemented in the form of a callback function.

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= > {
  / / the ios to the token
  bridge.callHandler('gettoken', {foo: 'bar'}, newtoken= > {
    resolve(newtoken)
  })
})
Copy the code

This is an example: obtaining login credential tokens from native IOS apps. And for better use, I wrapped a layer of Promise in order to be able to use the async/await syntax sugar in ES7 (gracefully handle async 😀), which I implemented very successfully when I excitedly opened the project in the IOS app’s WebView. Just when I thought I was on the safe side, the test told me he turned it on with a blank screen. My heart secretly ridicule, how possible, affirmation is you open posture wrong……

The analysis reason

How could the screen go blank? I didn’t have this situation. I went to the test with disdain and understood the method of reproduction. 😂 😂. It turns out that this project is not only used in our APP, but also needs to be opened by customers in other browsers, because some functions can be used without logging in. When I learned about the replay scenario, I immediately thought that there must be a mistake when I got the Token. However, when I opened the console, there was no error. After numerous debugge attempts, I finally found that the code did not enter the callback function at all.

Originally only in app need js injection, can call the relevant method, in other applications (browser, wechat… Because there is no injection method and no way to call it, eventually the code will not be able to execute into the callback function, and the resolve method will not be executed in the Pormise object I encapsulated. The final await blocked because it never returned, resulting in a final blank screen

Workaround – plus timeout handling

Timeout handling in callback functions

In a callback function, it’s easy to think of just adding a timed task to the asynchronous task. Asynchronous tasks and scheduled tasks will be executed at the same time. The one who completes the scheduled task first executes the callback function. If the asynchronous task does not return a result after the scheduled task is completed, it naturally times out

function timeourFn(cb,timeout){
  // Asynchronous tasks
  async((res) = >{
    cb(res)
  })
  // Timeout task
  setTimeout(() = > {
    cb(new Error('timeout'))
  }, timeout);
}

timeourFn((res) = >{
  if(res instanceof Error) {//5 seconds after the timeout}},5*1000)
Copy the code

Timeout handling in promises

Unfortunately, the WebViewJavascriptBridge method is not our own implementation, so it doesn’t seem to work with callback functions. But we can implement it using the Race method in promises. The promise.race () method wraps multiple Promise instances into a new Promise instance, and as literal as it is, the “race” is the essence of this method. As soon as one of the promises executes first (either resolved or rejected), promise.race () has a result (resolved or rejected), and it’s easy to figure out how to solve the problem.

  1. Change the token fetching method to a Promise object, and require an additional Promise object for the timeout task
  2. Place the two Promise objects in an array and execute them in the RACE method.
function getToken(timeout){
  return Promise.race([
    new Promise((resolve, reject) = > {
        setupWebViewJavascriptBridge(bridge= > {
          bridge.callHandler('gettoken', {foo: 'bar'}, newtoken= > {
            resolve(newtoken)
          })
        })
    }),
    // Timeout task
    new Promise((resolve, reject) = > {
      setTimeout(() = > {
        reject(new Error('timeout'))
      }, timeout)
    })
  ])
}
Copy the code

When the getToken does not return the token after timeout milliseconds, the second Promise object in the race method’s argument array completes, executes Reject, and throws a timeout exception. At this point, executing getToken in business code will no longer block subsequent code, but will be caught for exception handling.