天天看點

JavaScript中的async/await

1. async/await是什麼?

async 是一個修飾符,async 定義的函數會預設的傳回一個Promise對象resolve的值,是以對async函數可以直接進行then操作,傳回的值即為then方法的傳入函數。

await 也是一個修飾符,await 關鍵字 隻能放在 async 函數内部, await關鍵字的作用 就是擷取 Promise中傳回的内容, 擷取的是Promise函數中resolve或者reject的值。

那麼async/await到底是幹嘛的呢?

1.async/await 是一種編寫異步代碼的新方法。之前異步代碼的方案是回調和 promise。

2.async/await 是建立在 promise 的基礎上。(如果對Promise不熟悉,我已經着手在寫Promise的文章了)

3.async/await 像 promise 一樣,也是非阻塞的。

4.async/await 讓異步代碼看起來、表現起來更像同步代碼。這正是其威力所在。

async/await其實是Promise的文法糖,它能實作的效果都能用then鍊來實作,這也和我們之前提到的一樣,它是為優化then鍊而開發出來的。從字面上來看,async是“異步”的簡寫,await譯為等待,是以我們很好了解async聲明function是異步的,await等待某個操作完成。當然文法上強制規定await隻能出現在asnyc函數中,我們先來看看async函數傳回了什麼:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)           

複制

JavaScript中的async/await

這個async聲明的異步函數把return後面直接量通過Promise.resolve()傳回Promise對象,是以如果這個最外層沒有用await調用的話,是可以用原來then鍊的方式來調用的:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   //hello world
})           

複制

聯想一下Promise特點——異步無等待,是以當沒有await語句執行async函數,它就會立即執行,傳回一個Promise對象,非阻塞,與普通的Promise對象函數一緻。

以下是一個promise在1s之後resolve的例子:

async function f() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve('done!'), 1000)
    })
    let result = await promise // 直到promise傳回一個resolve值(*)
    console.log(result) 
}
f()           

複制

函數執行到(*)行會‘暫停’,當promise處理完成後重新恢複運作, resolve的值成了最終的result,是以上面的代碼會在1s後輸出’done!’

我們強調一下:await字面上使得JavaScript等待,直到promise處理完成,

然後将結果繼續下去。這并不會花費任何的cpu資源,因為引擎能夠同時做其他工作:執行其他腳本,處理事件等等。

這隻是一個更優雅的得到promise值的語句,它比promise更加容易閱讀和書寫。

如果去掉await,我們獲得的就是一個promise對象。

//加上await
done!

//沒有await
Promise {<pending>}
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: "done!"           

複制

重點就在await,它等待什麼呢?

按照文法說明,await等待的是一個Promise對象,或者是其他值(也就是說可以等待任何值),如果等待的是Promise對象,則傳回Promise的處理結果;如果是其他值,則傳回該值本身。并且await會暫停目前async function的執行,等待Promise的處理完成。若Promise正常處理(fulfillded),其将回調的resolve函數參數作為await表達式的值,繼續執行async function;若Promise處理異常(rejected),await表達式會把Promise異常原因抛出;另外如果await操作符後面的表達式不是一個Promise對象,則傳回該值本身。

2. 深入了解async/await

我們來詳細說明一下async/await的作用。await操作符後面可以是任意值,當是Promise對象的時候,會暫停async function執行。也就是說,必須得等待await後面的Promise處理完成才能繼續:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之後出現hello world
}
testAwt();           

複制

await 表達式的運算結果取決于它等的東西。

如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。

如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的代碼,等着 Promise 對象 resolve,然後得到 resolve 的值,作為 await 表達式的運算結果。

我們再把上面的代碼修改一下,好好體會“阻塞”這個詞

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之後出現hello world
  console.log('Y')   // 3秒鐘之後出現Y
}
testAwt();
console.log('L')  //立即輸出L           

複制

這就是 await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它内部所有的阻塞都被封裝在一個 Promise 對象中異步執行。await暫停目前async的執行,是以’tangSir’'最先輸出,hello world’和‘tangj’是3秒鐘後同時出現的。

為什麼會立即輸出L,這就涉及到了JS中的事件循環了,我寫了一篇關于事件循環的部落格,看了應該會明白,總的來說,異步函數會在非異步函數之後運作。

3. async和await簡單應用

上面已經說明了 async 會将其後的函數(函數表達式或 Lambda)的傳回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,并将其 resolve 的結果傳回出來。

現在舉例,用 setTimeout模拟耗時的異步操作,先來看看不用 async/await 會怎麼寫

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v); //一秒鐘後輸出got long_time_value
});           

複制

如果改用 async/await 呢,會是這樣

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);  // 一秒鐘後輸出long_time_value
}

test();           

複制

tankLongTime()本身就是傳回的 Promise 對象,是以加不加 async結果都一樣。

4. 處理then鍊

前面我們說了,async和await是處理then鍊的文法糖,現在我們來看看具體是怎麼實作的:

假設一個業務,分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結果。我們仍然用setTimeout來模拟異步操作:

/**
 * 傳入參數 n,表示這個函數執行的時間(毫秒)
 * 執行的結果是 n + 200,這個值将用于下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}           

複制

現在用 Promise 方式來實作這三個步驟的處理。

function doIt(){
    console.time('doIt');
    let time1 = 300;
    step1(time1)
        .then((time2) => step2(time2))
        .then((time3) => step3(time3))  
        .then((result) => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        })
}

doIt();

//執行結果為:
//step1 with 300
//step2 with 500
//step3 with 700
//result is 900
//doIt: 1510.2490234375ms           

複制

輸出結果 result 是 step3() 的參數 700 + 200 = 900。doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一緻。

如果用 async/await 來實作呢,會是這樣:

async function doIt() {
    console.time('doIt');
    let time1 = 300;
    let time2 = await step1(time1);//将Promise對象resolve(n+200)的值賦給time2
    let time3 = await step1(time2);
    let result = await step1(time3);
    console.log(`result is ${result}`);
    console.timeEnd('doIt');
}

doIt();

//執行結果為:
//step1 with 300
//step2 with 500
//step3 with 700
//result is 900
//doIt: 1512.904296875ms           

複制

顯然我們用async/await簡單多了。

5. Promise處理結果為rejected

await 指令後面的 Promise 對象,運作結果可能是 rejected,是以最好把 await 指令放在 try…catch 代碼塊中。

async function myFunction() {
    try {
        await somethingThatReturnAPromise();
    } catch (err){
        console.log(err);
    }
}

//另一種寫法
async function myFunction() {
    await somethingThatReturnAPromise().catch(function(err) {
        console.log(err);
    })
}           

複制

6.執行個體

Vue普通寫法:

methods: {
     getLocation(phoneNum) {
         return axios.post('/mm接口', {
             phoneNum
         })
     },    
     getFaceList(province, city) {
         return axios.post('/nn接口', {
             province,
             city
         })
     },  
     getFaceResult () {
          this.getLocation(this.phoneNum).then(res => {
              if (res.status === 200 && res.data.success) {
              let province = res.data.obj.province;
              let city = res.data.obj.city;
                  this.getFaceList(province, city).then(res => {
                        if(res.status === 200 && res.data.success) {
                             this.faceList = res.data.obj
                        }
                  })
              }
         }).catch(err => {
             console.log(err)
         })     
     }
}           

複制

這時你看到了then 的鍊式寫法,有一點回調地域的感覺。現在我們在有async/ await 來改造一下。

加入async/ await:

首先把 getFaceResult 轉化成一個async 函數,就是在其前面加async, 因為它的調用方法和普通函數的調用方法是一緻,是以沒有什麼問題。然後就把 getLocation 和getFaceList 放到await 後面,等待執行, getFaceResult 函數修改如下:

async getFaceResult () {
                let location = await this.getLocation(this.phoneNum);
                if (location.data.success) {
                    let province = location.data.obj.province;
                    let city = location.data.obj.city;
                    let result = await this.getFaceList(province, city);
                    if (result.data.success) {
                        this.faceList = result.data.obj;
                    }
                }
            }           

複制

現在代碼的書寫方式,就像寫同步代碼一樣,沒有回調的感覺,非常舒服。

現在就還差一點需要說明,那就是怎麼處理異常,如果請求發生異常,怎麼處理? 它用的是try/catch 來捕獲異常,把await 放到 try 中進行執行,如有異常,就使用catch 進行處理。

async getFaceResult () {
                try {
                    let location = await this.getLocation(this.phoneNum);
                    if (location.data.success) {
                        let province = location.data.obj.province;
                        let city = location.data.obj.city;
                        let result = await this.getFaceList(province, city);
                        if (result.data.success) {
                            this.faceList = result.data.obj;
                        }
                    }
                } catch(err) {
                    console.log(err);
                }
            }           

複制

注:

ES7引入的關鍵字async/await是對JavaScript異步程式設計的改進。它可以使代碼更容易閱讀和調試。然而,為了正确使用它們,必須完全了解promise,因為它們隻不過是文法糖,而潛在的技術仍然是promise。