幫助你開始理解async/await

thomaszhou發表於2018-04-29

一邊學習前端,一邊通過部落格的形式自己總結一些東西,當然也希望幫助一些和我一樣開始學前端的小夥伴。

如果出現錯誤,請在評論中指出,我也好自己糾正自己的錯誤

幫助你開始理解async/await

author: thomaszhou

讓我們開始學習async和await

async/await使用同步的思維,來解決非同步的問題。

  • async的優點

    • 利用async建立的函式也是非同步函式,就像setTimeout那種一樣
    • async/await 的優勢在於處理 then 鏈:
      • 如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了,因為promise引數傳遞太麻煩了,而async/await特別方便
      • async可以直接接收傳遞的變數,但是peomise的then是獨立作用於,如果要取值,就要將部分資料暴露在最外層,在 then 內部賦值一次.
  • 相比較generator

    • (1)內建執行器。 Generator 函式的執行必須靠執行器,所以才有了 co 函式庫,而 async 函式自帶執行器。也就是說,async 函式的執行,與普通函式一模一樣,只要一行
    • (2)更好的語義。 async 和 await,比起星號和 yield,語義更清楚了。async 表示函式裡有非同步操作,await 表示緊跟在後面的表示式需要等待結果。
    • (3)更廣的適用性。 co 函式庫約定,yield 命令後面只能是 Thunk 函式或 Promise 物件,而async 函式的 await 命令後面,可以跟 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。

同步和非同步的理解:當我們發出了請求,並不會等待響應結果,而是會繼續執行後面的程式碼,響應結果的處理在之後的事件迴圈中解決。那麼同步的意思,就是等結果出來之後,程式碼才會繼續往下執行。

我們可以用一個兩人問答的場景來比喻非同步與同步。A向B問了一個問題之後,不等待B的回答,接著問下一個問題,這是非同步。A向B問了一個問題之後,然後就笑呵呵的等著B回答,B回答了之後他才會接著問下一個問題,這是同步。

1、安裝支援

babel已經支援,所以我們可以在webpack中使用 首先在當前專案中使用npm下載babel-loader。

npm install babel-loader --save-dev
複製程式碼

然後在配置文件webpack.confing.dev.js中配置在module.exports.module.rules中新增如下配置元素即可。

  {
    test: /\.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    options: {
      cacheDirectory: true,
    },
  },
複製程式碼

如果你使用最新版本的create-react-app或者vue-cli來構建你的程式碼,那麼它們應該已經支援了該配置。

2、普通宣告和await使用

  • async函式實際上返回的是一個Promise物件
async function fn() {
    return 30;
}

// 或者
const fn = async () => {
    return 30;
}
複製程式碼

在宣告函式時,前面加上關鍵字async,這就是async的用法。當我們用console.log列印出上面宣告的函式fn,我們可以看到如下結果:

console.log(fn());

//result
Promise = {
    __proto__: Promise,
    [[PromiseStatus]]: "resolved",
    [[PromiseValue]]: 30
}
複製程式碼

很顯然,fn的執行結果其實就是一個Promise物件。因此我們也可以使用then來處理後續邏輯。

fn().then(res => {
    console.log(res);  // 30
})
複製程式碼

await的使用-------------------

await的含義為等待。就是程式碼需要等待await後面的函式執行完並且有了返回結果之後,才繼續執行下面的程式碼。這正是同步的效果

但是我們需要注意的是,await關鍵字只能在async函式中使用。並且await後面的函式執行後必須返回一個Promise物件才能實現同步的效果

當我們使用一個變數去接收await的返回值時,如:const temp = await fn();該返回值temp為Promise中resolve出來的值(也就是PromiseValue)

// 定義一個返回Promise物件的函式
function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(30);
        }, 1000);
    })
}

// 然後利用async/await來完成程式碼
const foo = async () => {
    const t = await fn(); // 將30傳入
    console.log(t);
}

foo();
console.log('begin')

// begin
// 30
複製程式碼

首先我們定義了一個函式fn(),這個函式返回Promise,並且會延時 1 秒,resolve並且傳入值30,foo函式在定義時使用了關鍵字async,然後函式體中配合使用了await,最後執行foo()。整個程式會在 1 秒後輸出30,也就是說foo()中常量t取得了fn()中resolve的值,並且通過await阻塞了後面的程式碼執行,直到fn()這個非同步函式執行完。

執行這個例子我們可以看出,當在async函式中,執行遇到await時,就會等待await後面的函式執行完畢,而不會直接執行next code。

可以看到begin優先輸出,是因為async/await建立的foo()函式也是非同步函式,所以你懂的

如果我們直接使用promise的then方法的話,想要達到同樣的結果,就不得不把後續的邏輯寫在then方法中。

const foo = () => {
    return fn().then(t => {
        console.log(t);
        console.log('next code');    
    })
}
foo();
複製程式碼

很顯然如果使用async/await的話,程式碼結構會更加簡潔,邏輯也更加清晰。

從程式碼片段中不難看出 Promise 沒有解決好的事情,比如要有很多的 then 方法,整塊程式碼會充滿 Promise 的方法,而不是業務邏輯本身.

而且每一個 then 方法內部是一個獨立的作用域,要是想共享資料,就要將部分資料暴露在最外層,在 then 內部賦值一次.

雖然如此,Promise 對於非同步操作的封裝還是非常不錯的,所以 async/await 是基於 Promise 的,await 後面是要接收一個 Promise 例項。

3、異常處理

在Promise中,我們知道是通過catch的方式來捕獲異常。而當我們使用async時,則通過try/catch來捕獲異常。

function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error.');
        }, 1000);
    })
}

const foo = async () => {
    try {
        await fn();
    } catch (e) {
        console.log(e);  // some error
    }
}

foo();
複製程式碼

await 命令後面的 Promise 物件,執行結果可能是 rejected,所以最好把 await 命令放在 try...catch 程式碼塊中

如果有多個await函式,那麼只會返回第一個捕獲到的異常

function fn1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn1.');// 設定reject
        }, 1000);
    })
}
function fn2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn2.'); // 設定reject
        }, 1000);
    })
}

const foo = async () => {
    try {
        await fn1();
        await fn2();
    } catch (e) {
        console.log(e);  // some error fn1.
    }
}

foo();
複製程式碼
async

4、async/await 的優勢在於處理 then 鏈

單一的 Promise 鏈並不能發現 async/await 的優勢,但是,如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了(很有意思,Promise 通過 then 鏈來解決多層回撥的問題,現在又用 async/await 來進一步優化它)。

例子一:

假設一個業務,分多個步驟完成,每個步驟都是非同步的,而且依賴於上一個步驟的結果。我們仍然用 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");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

複製程式碼
  • async/await 方式來實現這三個步驟的處理
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();
複製程式碼

結果和之前的 Promise 實現是一樣的,但是這個程式碼看起來是不是清晰得多,幾乎跟同步程式碼一樣

例子二:

現在把業務要求改一下,仍然是三個步驟,但每一個步驟都需要之前每個步驟的結果。

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

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

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}
複製程式碼
  • 用 async/await 來寫:
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();
複製程式碼

除了覺得執行時間變長了之外,似乎和之前的示例沒啥區別啊!別急,認真想想如果把它寫成 Promise 方式實現會是什麼樣子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();
複製程式碼

有沒有感覺有點複雜的樣子?那一堆引數處理,就是 Promise 方案的死穴—— 引數傳遞太麻煩了,看著就暈!

5、await in for 迴圈

  • await 命令只能用在 async 函式之中,如果用在普通函式,就會報錯。(注意想forEach,map,reduce這種也是函式!!!!)
    • 正確的寫法是採用 for 迴圈。!!!!
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}
複製程式碼

上面程式碼會報錯,因為 await 用在普通函式之中了。但是,如果將 forEach 方法的引數改成 async 函式,也有問題。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 可能得到錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}
複製程式碼

上面程式碼可能不會正常工作,原因是這時三個 db.post 操作將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用 for 迴圈

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}
複製程式碼
  • 如果確實希望多個請求併發執行,可以使用 Promise.all 方法
    • 先將多個函式(任務)都儲存到doc這個陣列中,就可以儲存多個任務,然後再實現併發執行
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的寫法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  // 將多個函式(任務)都儲存到doc這個陣列中,就可以儲存多個任務,然後再實現併發執行
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}
複製程式碼

6、實踐

在實踐中我們遇到非同步場景最多的就是介面請求,那麼這裡就以jquery中的$.get為例簡單展示一下如何配合async/await來解決這個場景。

// 先定義介面請求的方法,由於jquery封裝的幾個請求方法都是返回Promise例項,因此可以直接使用await函式實現同步
const getUserInfo = () => $.get('xxxx/api/xx');

const clickHandler = async () => {
    try {
        const resp = await getUserInfo();
        // resp為介面返回內容,接下來利用它來處理對應的邏輯
        console.log(resp);

        // do something
    } catch (e) {
        // 處理錯誤邏輯
    }
}
複製程式碼

7、一個問題測試

題目

可修改下面的 aa() 函式,目的是在一秒後用 console.log() 輸出 want-value

function aa() {
    setTimeout(function() {
        return "want-value";
    }, 1000);
}
複製程式碼
  • 但是,有額外要求:
    • aa() 函式可以隨意修改,但是不能有 console.log()
    • 執行 console.log() 語句裡不能有 setTimeout 包裹

解答

問題的主要目的是考察對非同步呼叫執行結果的處理,既然是非同步呼叫,那麼不可能同步等待非同步結果,結果一定是非同步的

setTimeout() 經常用來模擬非同步操作。最早,非同步是通過回撥來通知(呼叫)處理程式處理結果的

function aa() {
    return new Promise((resolve) => {
      setTimeout(function() {
        resolve('want-value');
			},1000);
		});
  }

async function fn() {
    let temp = await aa();
    console.log(temp);
}
fn();
複製程式碼

參考文章:

相關文章