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) {
    var xhr = new XMLHttpRequest()
    xhr.open('get', 'http://localhost:3000/'+path)
    xhr.send(null)
    var ret = null
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
            // 獲取后台數據
            ret = xhr.responseText
        }
    }
    return ret
}

let res = queryData('data1')
console.log(res)  // 結果為:undefined

  要想執行異步操作代碼的返回內容,就需要使用回調函數,下面介紹一種正確的封裝方法:

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')
})

  雖然使用了promise對象,但是一路通過 . 調用方法進行下去,代碼的可讀性較差。

 

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
})


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM