Asynchronous Request Callback Nested Solution

Posted by ltoto on Fri, 09 Aug 2019 09:06:31 +0200

Preface

In front-end asynchronous requests, the traditional way to get data is to write ajax callbacks, but this way
This is not conducive to the maintenance and readability of code, so the Generator and Proise solved this problem in the es6 era, and es7 made it more concise through async and await.

Replace callback nesting with Generator

Generator's feature is that after initialization, calling a next method pauses before the yield keyword, and it can also pass values in the next method so that the corresponding yield statement can be obtained.

  1. Write the preparation code first
const axios = require('axios')

const http = axios.create({
  baseURL: 'http://127.0.0.1:89',
  timeout: 3000,
  headers: {'Accept': 'application/json'}
});


let it;

Defined it is the meaning of iterator, which is not yet valued

  1. Asynchronous request invocation method
function call(url,options={}) {
    setTimeout(()=>{
        if ( !it ) {
            throw new Error('Initialize the generator')
        }
        http(url,options)
        .then(v=>{
            
            it.next(v.data)  // The generator passes parameters and starts the next execution
        })
    },0)
}

Wrapping with setTimeout ensures that the code is executed asynchronously. If not, it judgments executed synchronously may throw exceptions.

  1. Write the generator, here is the main asynchronous request logic processing
/**
    Multiple requests are interdependent
    1. Get user credentials based on user name and password
    2. Get a list of user data IDS based on user credentials
    3. Get the first data detail in the data list
*/
function * getData() {
    const data1 = yield call('/login', { method: 'POST', data: JSON.stringify({user:'root',pass:'123456'}) })
    console.log('I am the result 1:',data1)
    const data2 = yield call('/list', { method: 'POST', headers: { token: data1.data.token } })
    console.log('I am the result 2:',data2)
    const data3 = yield call('/item', { method: 'POST', headers: { token: data1.data.token }, data: JSON.stringify({ id:data2.data.ids[0] }) })
    console.log('I am the result 3:',data3)  
}
  1. Call, run
it = getData()
it.next()

Attach a value to it, and then trigger the code for step 2

Running process

First, we define the generator. When we run it, we only need to execute it.netx. Then we run the statement after the first yield and stop at the first yield statement. When the asynchronous request in the call function is executed, the result of the asynchronous request it.next(data) is passed to the value statement before the first yield. Execute the call after the second yield statement, and so on until the entire generator has finished executing

Generator + Promise

Generator alone can solve most of the asynchronous nesting problems, but it is not perfect enough. To ensure it initialization, the whole call must be asynchronously executed, the code is not elegant enough, and depends on external it, the structure is decentralized, so we can further improve it with Generator + Promise.

  1. Simplified call method

    function call(url,options={}) {
      return http(url,options)
    }

Remove it from the call and execute it.next.

  1. Added external call generator next function run

    function run (g) {
      const it = g();  // Initialize the generator. Notice the colon here.
      
      (function each(res) {
          // Judgment based on the returned result of the generator
          if (!res.done && res.value instanceof Promise ) {  // If Promise returns
              res.value.then(v=>{
                  each( it.next(v.data) )    // Here is the next in the call of Scheme 1 and pass the value to the next next one. 
              })
          } else if (res.done) {    // Generator Execution Ends, Running Ends
              return
          } else {
              throw new Error('yield Please return later Promise A function of')
          }
      })(it.next())    //Self-operation
    }
  2. Function

    run(getData)

Running the getData generator in Method 1 yields the same results

  1. Function Extension after yield in getData

According to the run function, as long as the result returned by it.next is Promise, the following can be written in getData.

function * getData() {
  const data1 = yield http('/login', { method: 'POST', data: JSON.stringify({user:'root',pass:'123456'}) })
  console.log('I am the result 1:',data1)
  const data2 = yield http('/list', { method: 'POST', headers: { token: data1.data.token } })
  console.log('I am the result 2:',data2)
  const data3 = yield http('/item', { method: 'POST', headers: { token: data1.data.token }, data: JSON.stringify({ id:data2.data.ids[0] }) })
  console.log('I am the result 3:',data3)  
}

** The http function here is an example of axios with a return value of Promise.
The outer layer plus call can write some exceptions in the call, or test the handling, similar to React's dva handling **

ES7 Processing Mode

If you still find Scheme 2 a bit cumbersome, try the await grammar of ES7

  1. Modify the getData function as follows
/**
    Multiple requests are interdependent
    1. Get user credentials based on user name and password
    2. Get a list of user data IDS based on user credentials
    3. Get the first data detail in the data list
*/
async function getData() {
    const { data:data1 } = await call('/login', { method: 'POST', data: JSON.stringify({user:'root',pass:'123456'}) })
    console.log('I am the result 1:',data1)
    const { data:data2 } = await call('/list', { method: 'POST', headers: { token: data1.data.token } })
    console.log('I am the result 2:',data2)
    const { data:data3 } = await call('/item', { method: 'POST', headers: { token: data1.data.token }, data: JSON.stringify({ id:data2.data.ids[0] }) })
    console.log('I am the result 3:',data3)  
}

Compared with scheme 2, this method has more async keywords in its header, eliminates the * sign, and replaces yield with await, which is the declaration way of ES7 asynchronous function.
Note that the return value is not injected through next res.data in scenario 2, so the whole res is obtained. When taking the value, pay attention to the. data data data in the result.

  1. Function
getData()

As a result, this approach is simpler and easier to understand, as in Plan 12.

Solution 3 Simple and complete code

Personal prefers concise and efficient code, so recommend solution 3

const axios = require('axios')

const http = axios.create({
  baseURL: 'http://127.0.0.1:89',
  timeout: 3000,
  headers: {'Accept': 'application/json'}
});


/**
    Multiple requests are interdependent
    1. Get user credentials based on user name and password
    2. Get a list of user data IDS based on user credentials
    3. Get the first data detail in the data list
*/
async function getData() {
    const { data:data1 } = await http('/login', { method: 'POST', data: JSON.stringify({user:'root',pass:'123456'}) })
    console.log('I am the result 1:',data1)
    const { data:data2 } = await http('/list', { method: 'POST', headers: { token: data1.data.token } })
    console.log('I am the result 2:',data2)
    const { data:data3 } = await http('/item', { method: 'POST', headers: { token: data1.data.token }, data: JSON.stringify({ id:data2.data.ids[0] }) })
    console.log('I am the result 3:',data3)  
}

getData() //Function

The above code only completes the core functions, some defensive and exception handling is not perfect, only for understanding and learning.

Topics: Javascript JSON axios React