異步編程終極大法async/await入門

前言

async 函數(shù)是目前解決JS異步編程的終極解決方案,學(xué)習(xí)它之前你可以先看看我寫的我對(duì)Promises的理解理解 ES6 Generator 函數(shù),因?yàn)閍sync/await需要聯(lián)合Promises使用,而且是Generator函數(shù)的語法糖,所以必須先學(xué)Promises和Generator,最少,需要先學(xué)Promises。

async函數(shù)在較低版本的Chrome瀏覽器中,需要開啟“實(shí)驗(yàn)性 JavaScript”才能使用,55版本以上默認(rèn)支持。

例1:a/a的最簡(jiǎn)單舉例

一個(gè)最簡(jiǎn)單的使用a/a的范例如下:

function type(delayedTime) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(delayedTime);
            resolve();
        }, delayedTime);
    });
}

async function asyncFunc() {
    await type(3000);
    await type(2500);
    console.log(1);
};
asyncFunc();

最終執(zhí)行結(jié)果是:等三秒,打印3000,然后等2.5秒,打印2500,然后緊跟輸出1。

一、先看type函數(shù),他是一個(gè)普通函數(shù),內(nèi)部返回一個(gè)Promise對(duì)象。除此之外沒有特別的。

二、然后看asyncFunc函數(shù),它不是普通函數(shù),它前面有async關(guān)鍵字,這意味著asyncFunc函數(shù)是一個(gè)異步函數(shù),async就是異步的意思,很好理解,比Promise(允諾)和Generator(生成器)要好理解的多。

三、看async函數(shù)的內(nèi)部,async函數(shù)的內(nèi)部一定會(huì)有await關(guān)鍵字,await也容易理解,就是等待。也就是說,等await后面的表達(dá)式執(zhí)行完了,才執(zhí)行下一個(gè)表達(dá)式。

最后可以看出,雖然我說你必須先學(xué)Promises和Generator,但是這不意味著a/a最難學(xué),相反,它才是最好學(xué)的,因?yàn)樗耆系谝惶鞂W(xué)習(xí)JS的初學(xué)者對(duì)JS的錯(cuò)誤理解:

“我寫JS只需要從上到下一行一行的寫下去,代碼就肯定會(huì)是一行一行的從上到下執(zhí)行下去,每一句都會(huì)等上一句執(zhí)行完了才執(zhí)行?!?/p>

雖然這種理解絕對(duì)是錯(cuò)誤的認(rèn)知,但是,其實(shí),這才是異步編程的最理想解決方案:“你根本看不出它是異步代碼,你只需要像寫同步代碼一樣寫異步代碼就好了?!?/p>

總結(jié)起來,a/a大法的最基本用法就是:

1. 首先定義一個(gè)或多個(gè)普通函數(shù),函數(shù)必須返回的是Promise對(duì)象(事實(shí)上你可以返回其他數(shù)據(jù),但這就失去了a/a的威力)。Promise對(duì)象中可以寫任意異步語句,必須有resolve()。

2. 然后定義一個(gè)async函數(shù),函數(shù)語句就是執(zhí)行那些個(gè)普通函數(shù),注意,每個(gè)執(zhí)行語句前面都加上await關(guān)鍵字。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。(await后面也可以跟原始類型,但沒意義。)

3. 執(zhí)行這個(gè)async函數(shù)即可。async函數(shù)的返回值是Promise對(duì)象,你可以用Promise對(duì)象的.then()方法指定下一步的操作。

例2:把例1用Generator函數(shù)寫法寫出來

function type(delayedTime) {
    setTimeout(function () {
        console.log(delayedTime);
        it.next();
    }, delayedTime);
}

function* asyncFunc() {
    yield type(3000);
    yield type(2500);
    console.log(1);
}

var it = asyncFunc();

it.next();

可以看出a/a跟Generator函數(shù)的區(qū)別:

  1. Generator函數(shù)的執(zhí)行必須靠執(zhí)行器(例子中是it.next()),每一個(gè)普通函數(shù)中通常會(huì)有一個(gè)執(zhí)行器,多個(gè)普通函數(shù)中有多個(gè)執(zhí)行器,實(shí)踐中如果偶爾忘了寫執(zhí)行器,那么整個(gè)代碼就不會(huì)正常執(zhí)行;而async函數(shù)自帶執(zhí)行器。也就是說,async函數(shù)的執(zhí)行,與普通函數(shù)一模一樣,只要一行asyncFunc()。

  2. async和await,比起星號(hào)和yield,語義更清楚。

  3. async函數(shù)的返回值是Promise對(duì)象,這比Generator函數(shù)的返回值是迭代器對(duì)象方便得多。

例3:把例1用Promises方式寫出來

function type(delayedTime) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(delayedTime);
            resolve();
        }, delayedTime);
    });
}

type(3000)
.then(function() {
    return type(2500);
})
.then(function(){
    console.log(1);
});

可以看出a/a與Promises寫法的區(qū)別:

  1. Promises寫法每一步要為下一步的.then返回一個(gè)Promise對(duì)象,注意,return type(1000);return不要忘了寫,不要以為函數(shù)里有return,這里就可以省略。而a/a寫法,在async函數(shù)中不需要有return關(guān)鍵字。

  2. Promises寫法每一個(gè).then都要接收一個(gè)回調(diào)函數(shù)作為參數(shù),而a/a完全不用這么麻煩,await等待的雖然是promise對(duì)象,但不必寫.then(..),直接可以得到返回值。所以說await是then的語法糖。

  3. 當(dāng)然了,async函數(shù)執(zhí)行之后,也返回一個(gè)Promise對(duì)象,也就是說,不光是async函數(shù)內(nèi)部的await有then的作用,而且async函數(shù)本身也可以連綴then,等于肚子里可以連綴then,肚子外還可以連綴then。

async函數(shù)連綴.then的最基本寫法

上面說了,async函數(shù)永遠(yuǎn)返回一個(gè)Promise對(duì)象,所以它當(dāng)然可以連綴.then方法。

復(fù)習(xí)一下剛才的命題,3秒之后打印3000,然后2.5秒之后打印2500,然后立即打印1,對(duì)吧?現(xiàn)在我打算增加后續(xù):

打印1之后,再等2秒,打印2000,再等1.5秒,打印1500。

代碼如下:

function type(delayedTime) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(delayedTime);
            resolve(delayedTime);
        }, delayedTime);
    });
}

async function asyncFunc() {
    await type(3000);
    await type(2500);
    console.log(1);
};

asyncFunc()
.then(function () {
    return type(2000);
})
.then(function () {
    return type(1500);
});

關(guān)鍵點(diǎn)在于return關(guān)鍵字,它用于返回一個(gè)Promise對(duì)象。

async函數(shù)內(nèi)部await傳遞數(shù)據(jù)的寫法

現(xiàn)在的命題是,我想要先延遲3秒,打印3000,然后把3000這個(gè)數(shù)字減去500(也就是得到2500),作為下一個(gè)延遲的毫秒數(shù),然后打印這個(gè)毫秒數(shù),然后繼續(xù)減去500(也就是得到2000),作為再下一個(gè)延遲的毫秒數(shù),然后打印這個(gè)毫秒數(shù),然后再繼續(xù)減去500(也就是得到1500),然后打印這個(gè)毫秒數(shù)。

總共打印4個(gè)數(shù)。寫法如下:

function type(delayedTime) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(delayedTime);
            resolve(delayedTime);
        }, delayedTime);
    });
}

async function asyncFunc() {
    var t1 = await type(3000);
    var t2 = await type(t1 - 500);
    var t3 = await type(t2 - 500);
    type(t3 - 500);
};
asyncFunc();

關(guān)鍵點(diǎn)有2個(gè),一個(gè)是resolve(delayedTime),它用于發(fā)出數(shù)據(jù),一個(gè)是var t1 = await type(3000);,這里注意,type(3000)返回的是Promise對(duì)象,await返回的是delayedTime的值。

我們這個(gè)例子是每次減500,甚至可以使用for循環(huán)來執(zhí)行,并不會(huì)打亂順序。

async函數(shù)外部連綴then并且還要傳遞數(shù)據(jù)的寫法

除了內(nèi)部能傳遞數(shù)據(jù),外部也一樣可以。代碼如下:

function type(delayedTime) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(delayedTime);
            resolve(delayedTime);
        }, delayedTime);
    });
}

async function asyncFunc() {
    return await type(3000);
}

asyncFunc()
.then(function (result) {
    return type(result - 500);
})
.then(function (result) {
    return type(result - 500);
})
.then(function (result) {
    type(result - 500);
});

可以看出,外部連綴then的寫法,代碼比較冗余,所以盡量在內(nèi)部使用await實(shí)現(xiàn)。因?yàn)閍wait已經(jīng)有then的作用,沒必要在外部連綴then。

async函數(shù)有多種使用形式

函數(shù)聲明形式,也是最基本的形式

async function foo() {}

函數(shù)表達(dá)式形式

const foo = async function () {};

對(duì)象的方法形式

let obj = { async foo() {} };
obj.foo().then(...)

Class的方法

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

箭頭函數(shù)

const foo = async () => {};

錯(cuò)誤處理

在不考慮出錯(cuò)的情況下,async/await就以上這么點(diǎn)東西,其實(shí)就是async、await、return這三個(gè)關(guān)鍵字各種倒騰,每一步都要返回Promise對(duì)象就行了。

下面考慮錯(cuò)誤處理。

首先我還是假設(shè)你已經(jīng)懂了Promises的錯(cuò)誤處理機(jī)制。復(fù)習(xí)一下:

Promises利用reject()來傳遞錯(cuò)誤,利用.then的第二個(gè)回調(diào)函數(shù),或利用.catch來捕獲錯(cuò)誤。

a/a也是如此,舉個(gè)栗子:

function f() {
    return new Promise(function (resolve, reject) {
        reject('出錯(cuò)了');
    });
}

async function asyncFunc() {
    await f();
};

asyncFunc().then(function (e) {
    console.log(1111);
}, function (e) {
    console.log(e); // 出錯(cuò)了
});

asyncFunc().catch(function (e) {
    console.log(e); // 出錯(cuò)了
});

之前說過,await能夠起到.then的作用,await如果等待到的是失敗的結(jié)果,就必須先處理這個(gè)結(jié)果,才能進(jìn)入下一個(gè)流程。這一點(diǎn)很重要。

比如,上面例子如果我成這樣:

async function asyncFunc() {
    await f();
    await f();
};

第一個(gè)f()的執(zhí)行,只會(huì)得到失敗的結(jié)果,這個(gè)結(jié)果如果不被處理,那么第二個(gè)f()根本不會(huì)執(zhí)行。驗(yàn)證一下:

function f(data) {
    return new Promise(function (resolve, reject) {
        reject(data);
    });
}

async function asyncFunc() {
    await f('data1'); // await發(fā)現(xiàn)這個(gè)Promise的錯(cuò)誤沒有得到處理,所以不會(huì)進(jìn)入下個(gè)環(huán)節(jié)
    await f('data2'); // 那么這一步就不執(zhí)行
};

asyncFunc().catch(function (e) {
    console.log(e); // data1 也就是只會(huì)捕獲到第一個(gè)環(huán)節(jié)的錯(cuò)誤
});

現(xiàn)在我略加改動(dòng),給f('data1')做失敗處理,你再看看:

function f(data) {
    return new Promise(function (resolve, reject) {
        reject(data);
    });
}

async function asyncFunc() {
    await f('data1').catch(function (e) {
        console.log(e); // data1
    });
    await f('data2');
};

asyncFunc().catch(function (e) {
    console.log(e); // data2
});

所以你可以看到,async內(nèi)部也可以有.then和.catch級(jí)聯(lián),外部也可以繼續(xù)級(jí)聯(lián),a/a的宇宙,就是Promise的狀態(tài)和數(shù)據(jù)不斷傳遞的宇宙。a/a就是Promises的超集。

同時(shí)我們也可以看到,async內(nèi)部和外部都可以有.then和.catch級(jí)聯(lián),所以你就要思考到底是在內(nèi)部寫流程,還是外部寫流程,怎樣才能寫出最清晰的代碼。我個(gè)人建議優(yōu)先考慮在內(nèi)部寫小流程,在外部寫大流程。

談?wù)則ry {} catch(e) {}

你會(huì)在一些參考文章里看到try {} catch(e) {}的用法,它也用于在async函數(shù)內(nèi)部實(shí)現(xiàn)錯(cuò)誤捕獲。就比如下面這種:

function f(data) {
    return new Promise(function (resolve, reject) {
        reject(data);
    });
}

async function asyncFunc() {
    try {
        await f('data1');
    } catch(e) {
        console.log(e); // data1
    }

    await f('data2');
};

asyncFunc().catch(function (e) {
    console.log(e); // data2
});

也就是說,既然你認(rèn)為第一步可能出錯(cuò),那么就try一下,這個(gè)寫法當(dāng)然是可以運(yùn)行的,但是盡量不要這么做,因?yàn)樵赼/a領(lǐng)域,已經(jīng)有.then和.catch來負(fù)責(zé)錯(cuò)誤分支,就根本沒必要引入try {} catch(e) {}的寫法,這種寫法徒增代碼復(fù)雜度。

a/a與Promises特有方法的配合

了解Promises規(guī)范(可以參看我的http://www.lxweimin.com/p/b497eab58ed7),就會(huì)知道Promise.all()和Promise.race(),這兩個(gè)特有方法的用法見我寫的文檔,跟a/a的結(jié)合使用也很簡(jiǎn)單,比如Promise.all():

// 例1,有一個(gè)失敗就進(jìn)入下一個(gè)環(huán)節(jié)
function f(data) {
    return new Promise(function (resolve, reject) {
        reject(data);
    });
}

async function asyncF() {
    await Promise.all([f('data1'),f('data2')]).catch(function (e) {
        console.log(e); // data1
    });
};

asyncF();

// 例2,全部成功才進(jìn)入下一個(gè)環(huán)節(jié)
function g(data) {
    return new Promise(function (resolve, reject) {
        resolve(data);
    });
}

async function asyncG() {
    await Promise.all([g('data1'),g('data2')]).then(function (e) {
        console.log(e); // ["data1", "data2"]
    });
};

asyncG();

Promise.race()跟a/a的結(jié)合也很簡(jiǎn)單,本質(zhì)都是Promise對(duì)象的狀態(tài)變化和數(shù)據(jù)傳遞,不多說。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容