談及回調地獄發生得情況和解決辦法,就必須追溯到原生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 })