搞懂JavaScript異步請求,一篇文章就夠了

前言


javascript 語言執行環境是單線程,就是一次只能完成一個任務,如果同時有多個任務,必須排隊執行。這種模式的好處是實現較簡單,執行環境單純,缺點是只要有一個任務耗時長,後面的任務必須排隊,拖延整個程序的執行效率。

同步 & 異步


同步

  • 在主線程上排隊執行的任務,只有前一個任務執行完畢,才能繼續執行下一個任務。
  • 也就是一旦調用開始,就必須等待其返回結果,程序的執行順序和任務排列順序一致。

異步

  • 發出調用請求後,調用者不必等待其返回結果再次執行其他操作,只要發出請求後,即可繼續執行後續操作。
  • 異步任務不進入主線程,而進入任務隊列的任務,只有任務隊列通知主線程,某個任務可以執行了,該任務纔會進入主線程。
  • 程序的執行順序和任務排列順序不一致,是異步的。

異步請求在江湖中的地位非常重要,異步給JS帶來了更多的可能,我們是不是遇到過這樣的業務場景:後一個請求依賴前一個請求的結果,也就是一個請求依賴另一個請求,如果依賴層數再多一點,很容易出現回調地獄的現象。本文將介紹幾種異步請求的處理方式,拭目以待。


回調地獄


大家在學習 Jquery 的時候都應該接觸過回調函數,在這,再次總結一下到底什麼是回調函數?

知乎推薦回答:你去商場買衣服,剛好你的 size 沒有了,於是你把電話留給了店員,過幾天,那件衣服補貨了,店員給你打電話,你去店裏把它買下來。在這個例子裏,你的電話號碼就是回調函數,你把電話留下叫做登記回調函數,店裏補貨了這個事件觸發了回調函數,店員給你打電話是調用回調函數,你去店裏買下來是響應回調事件。

回調函數就是 任何一個 被以該回調函數爲其參數的其他方法調用的方法。

//callback
//讀./pakage.json的內容,寫入./p.json文件,讀取成功 2s後打印"ok"

const fs = require('fs')
fs.readFile('./pakage.json',(err,info) => {
    fs.writeFile('./p.json',info,(err) => {
        if(!err) {
            setTimeout(() => {
                console.log('ok')
            },2000)
        }
    })
})

以上代碼用來實現異步操作,沒有什麼問題,但如果業務不只是這兩個文件,那還要很多個回調函數的嵌套嗎?這樣的代碼可維護性,可讀性都變得很差,這就是回調地獄。


Promise對象


Promise對象,返回一個“異步承諾”,不管是請求成功還是出錯都會執行,promise 對象帶有 resolve 和 reject 方法,可以請求結果進行分開處理。promise對象有三種狀態:

  • pending:初始狀態
  • fulfilled:成功操作,爲表述方便,用 resolved 代替。
  • rejected:失敗操作

promise的一個便利是,針對多個異步請求的情況,可以使用 Promise.all 方法來確保各個請求的執行返回,避免了“回調地獄”。

pending 狀態可以轉換爲 fulfilled 或者 rejected ,並且只能轉換一次,如果 pending 轉化爲其中一種狀態,就不能轉換爲另一種狀態了,並且 fulfilled 和 rejected 狀態只能由 pending 轉化而來,兩者之間不能互相轉換。

<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//如果低版本瀏覽器不支持Promise,通過cdn這種方式
 <script type="text/javascript">
     function loadImg(src) {
         var promise = new Promise(function (resolve, reject) {
             var img = document.createElement('img')
             img.onload = function () {
                 resolve(img)
             }
             img.onerror = function () {
                 reject('圖片加載失敗')
             }
             img.src = src
         })
         return promise
     }
     var src = 'https://www.imooc.com/static/img/index/logo_new.png'
     var result = loadImg(src)
     result.then(function (img) {
         console.log(1, img.width)
         return img
     }, function () {
         console.log('error 1')
     }).then(function (img) {
         console.log(2, img.height)
     })
  </script>

promise 多個串聯操作

promise 可以做更多事情,比如,有若干異步任務,需要先做任務1,成功後做任務2,任何任務失敗則不再繼續並進行錯誤處理,要實現這樣的異步任務,如果不用 Promise對象,需要一層一層的嵌套回調。有了 Promise ,我們只需要簡單的寫

job1.then(job2).then(job3).catch(handleError);

then 方法可以被同一個 promise 調用多次,返回一個新的 promise 對象,因此可以通過鏈式調用 then 方法,避免了地獄回調的嵌套回調。

promise 常用方法

promise.all()

試想一個頁面聊天系統,需要從兩個不同的URL獲取用戶的個人信息和好友列表,這兩個任務是可以並行執行的,用 Promise.all() 實現如下。

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 獲得一個Array: ['P1', 'P2']
});

promise.race()

有些時候,同時發起兩個請求獲取信息,只需要獲取先返回的數據即可,這種情況下,用 Promise.race()實現。

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});

可能 p1 執行較快,promise 的 then() 將獲得結果 p1,p2 仍在繼續執行,但執行結果將被丟棄。


總結:

  • Promise.all接受一個promise對象的數組,待全部完成之後,統一執行success;
  • Promise.race接受一個包含多個promise對象的數組,只要有一個完成,就執行success

將上面例子做下修改,加深對這兩者的理解:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
    console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
    console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
Promise.race([result1, result2]).then(function (data) {
    console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})

Async/Await


雖然promise 很便利了,但在代碼簡潔方面,還有待優化,因此由了 async/await ,在多個請求情況下更是可以“鏈式”聲明,可以大大節省代碼量。

Async/Await 是什麼?

async本意是異步,而 await 可以認爲是 async wait 的縮寫,所以可以理解爲:async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。

另外,語法規定,await 只能出現在 async 函數中。

async起什麼作用?

async 函數是如何處理它的返回值的?我們寫一個 demo 測試一下

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);//Promise { 'hello async' }

async 函數返回的是一個 promise 對象,如果在函數中 return ,async 會通過 Promise.resolve() 封裝成 Promise 對象。但如果 不 return ,會出現什麼情況,很顯然,它會返回 :Promise.resolve(undefined)

Promise 的特點是:無等待,所以在沒有 await 的情況下執行 async 函數,會立即執行,返回一個 promise 對象,絕不會阻塞後面的語句,這和普通的 promise 對象函數沒啥區別,關鍵在於 await 做了什麼?

await 在等待一個 async 函數完成,因爲 async 函數返回一個 Promise 對象,所以 await 可以用於等待一個 async 函數的返回值,如果 await 等到了要等的東西,比如說是一個 promise 對象,await 會阻塞後面的代碼,等待 promise 對象 resolve ,然後得到 resolve 的值,作爲 await 表達式的運算結果。

var step1 = async function () {
    return new Promise(function (resolve, reject) {
        let st = setTimeout(() => {
            resolve('step1');
        }, 1000);
    });
}

var step2 = function () {
    return new Promise(function (resolve, reject) {
        let st = setTimeout(() => {
            resolve('step2')
        }, 1000);
    });
}

var step3 = function () {
    return new Promise(function (resolve, reject) {
        let st = setTimeout(() => {
            resolve('step3')
        }, 1000);
    })
}
async function test() {
    var data1 = await step1();
    console.log(data1);
    var data2 = await step2();
    console.log(data2);
    var data3 = await step3();
    console.log(data3)
}
test();

最後


以上就是對js中異步處理幾種方式的簡單理解,理解異步,有助於對代碼的進一步理解。 本文僅作爲筆者個人學習記錄,能力有限,如有錯誤,請指正。不勝感激~~~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章