vlambda博客
学习文章列表

Ajax请求回调地狱及解决方案(promise、async和await)

谈及回调地狱发生得情况和解决办法,就必须追溯到原生ajax请求。

// 服务器端接口

app.get('/data1', (req, res) => {

  res.send('hi')

})

app.get('/data2', (req, res) => {

  res.send('hello')

})

app.get('/data3', (req, res) => {

  res.send('nihao')

})

 

// 启动监听

app.listen(3000, () => {

  console.log('running...')

})

原生ajax请求步骤

var xhr = new XMLHttpRequest();

xhr.open('get','http://localhost:3000/data1');

xhr.send(null);

xhr.onreadystatechange = function() {

    if(xhr.readyState == 4 && xhr.status == 200) {

        // 获取后台数据

        var ret = xhr.responseText;

        console.log(ret)

    }

}

又因为发送请求的以上代码需要经过反复复用,我们就需要将它封装为函数,以减少代码的冗余度。下面请看两个封装方法(都是错误的封装):

  错误封装1:发生错误的原因是queryData函数本身没有返回值,会默认返回undefined,return ret是写在queryData的内层函数中的。

function queryData(path) {

    var xhr = new XMLHttpRequest();

    xhr.open('get','http://localhost:3000/'+path);

    xhr.send(null);

    xhr.onreadystatechange = function() {

        if(xhr.readyState == 4 && xhr.status == 200) {

            // 获取后台数据

            let ret = xhr.responseText;

            return ret;

        }

    }

}

 

let res = queryData(‘data1’);

console.log(res);  // 结果为:undefined

        这样很容易就让我们想到另一种封装方法——把ret在外层函数中返回。然而这就产生了另一种错误的封装效果。

        错误封装2:这种情况下发生错误的原因是ajax请求时异步的,数据ret还没有修改成功时,就已经执行了queryData函数的return代码。这时ret的值并没有被修改,当然还是null。

        要想执行异步操作代码的返回内容,就需要使用回调函数,下面介绍一种正确的封装方法:

function queryData(path, callback) {

  var xhr = new XMLHttpRequest();

  xhr.open('get','http://localhost:3000/' + path);

  xhr.send(null);

  xhr.onreadystatechange = function() {

    if(xhr.readyState == 4 && xhr.status == 200) {

      // 获取后台数据

      var ret = xhr.responseText;

      callback(ret);

    }

  }

}

 

queryData('data1',function(ret) {

  console.log(ret)    // 结果为:hi

})

        但是,如果想要按顺序获取接口'data1'、'data2'、'data3'中的数据,就会进行下面的操作,也就造成了回调地狱的问题。

queryData('data1'function(ret) {

  console.log(ret)   // 按顺序第一个输出为:hi

  queryData('data2'function(ret) {

    console.log(ret)  //按顺序第二个输出为:hello

    queryData('data3'function(ret) {

      console.log(ret)  // 按顺序第三个输出为:nihao

    });

  });

});

promise方式

为了改造上面的回调地狱问题,诞生了promise。promise其实就是一种语法糖(代码形式发生改变、但是功能不变)。

function queryData(path) {

  return new Promise(function(resolve, reject) {

    // 需要在这里处理异步任务

    var xhr = new XMLHttpRequest();

    xhr.open('get','http://localhost:3000/' + path);

    xhr.send(null);

    xhr.onreadystatechange = function() {

      // 该函数何时触发?xhr.readyState状态发生变化时

      if(xhr.readyState != 4) return;

      if(xhr.readyState == 4 && xhr.status == 200) {

        // 获取后台数据

        var ret = xhr.responseText;

        // 成功的情况

        resolve(ret);

      else {

        // 失败的情况

        reject('服务器错误');

      }

    }

  })

}

 

queryData('data1')

  .then(ret=>{

    console.log(ret)    // 按顺序第一个输出为:hi

    // 这里返回的是Promise实例对象,下一个then由该对象调用

    return queryData('data2');

  })

  .then(ret=>{

    console.log(ret);  // 按顺序第二个输出为:hello

    return queryData('data3');

  })

  .then(ret=>{

    console.log(ret)  // 按顺序第三个输出为:nihao

  })

        对于上面代码中使用.then调用的情况,有几点说明:

queryData('data1')

  .then(ret=>{

    console.log(ret)  // 顺序输出第一个结果为:hi

    // 如果在then方法中没有返回Promise实例对象,那么下一个then由默认产生的Promise实例对象调用

  })

  .then(ret=>{

    console.log('-------------------' + ret)  // 顺序输出第二个结果为:----------------------undefined

    // 如果在then中显式地返回一个具体数据,那么下一个then可以获取该数据

    return 456;

  })

    .then(ret=>{

    console.log('-------------------' + ret)  // 顺序输出第三个结果为:----------------------456

  })

上面的代码第二个.then中return的是456,为什么能继续调用.then方法呢?

  这是因为return 456 实际上可以理解为下面的三种表示方式:

// Promise.resolve的作用:就是把数据转化为Promise实例对象

 

// 方式一:

return Promise.resolve(456);

 

// 方式二:

return new Promise(function(resolve, reject) {

  resolve(99999);

})

 

// 方式三:

Promse.resolve = function(param) {

  return new Promise(function(resolve, reject) {

    resolve(param);

  })

}

return Promise.resolve(88888);

promise对象除了.then方法外还有两个方法可以通过 . 调用,其中.finally是ES7中新增的方法。

.catch(ret=>{

  // 发生错误时触发

  console.log('error')

})

.finally(ret=>{

  // 无论结果成功还是失败都触发:一般用于释放一些资源

  console.log('finally')

})

async和await

下面我们就提出解决回调地狱最好的一种方法,通过使用 async 和 await 

function queryData(path) {

  return new Promise(function(resolve, reject) {

    // 需要在这里处理异步任务

    var xhr = new XMLHttpRequest();

    xhr.open('get','http://localhost:3000/' + path);

    xhr.send(null);

    xhr.onreadystatechange = function() {

      // 当readyState值不为0的时候直接返回

      if(xhr.readyState != 4) return;

      if(xhr.readyState == 4 && xhr.status == 200) {

        // 获取后台数据

        var ret = xhr.responseText;

        // 成功的情况

        resolve(ret);

      else {

        // 失败的情况

        reject('服务器错误');

      }

    }

  })

}

 

async function getAllData() {

  // await执行流程是顺序执行

  let ret1 = await queryData('data1');

  let ret2 = await queryData('data2');

  let ret3 = await queryData('data3');

  console.log(ret1)

  console.log(ret2)

  console.log(ret3)

}

getAllData();

另外,有一点需要提起注意:async函数的返回值是Promise实例对象

async function getAllData() {

  // await执行流程是顺序执行

  let ret1 = await queryData('data1');

  return 'hello';

}

var ret = getAllData();

console.log(ret)  // 这里输出一个promise对象,并且resolve的数据为hello

ret.then(res=>{

  console.log(res)  // 这里输出结果为:hello

})