ES6中的異步

async.jpg

上個(gè)寒假又又又···不知道第幾遍重溫ES6,以下對(duì)幾個(gè)異步實(shí)現(xiàn)寫(xiě)個(gè)總結(jié)。

在之前,我們最常見(jiàn)的異步實(shí)現(xiàn)莫過(guò)于回調(diào)函數(shù),但是如果嵌套太深,代碼就會(huì)變得難以閱讀與維護(hù)。而在ES6中,我們有了更多更加優(yōu)美的異步實(shí)現(xiàn)方式,正因如此,我們才得以走出回調(diào)地獄。

Promise

Promise是異步編程的一種優(yōu)良的解決方案,它比傳統(tǒng)的回調(diào)更加合理。在ES6中,Promise是一個(gè)對(duì)象,它有三種狀態(tài):進(jìn)行中(pending)、已成功(resolved)、已失?。╮ejected)。所以Promise可能的改變就只有兩種,從進(jìn)行中轉(zhuǎn)變到已成功或者從進(jìn)行中轉(zhuǎn)變到失敗,并且一旦狀態(tài)發(fā)生改變,之后便會(huì)一直保持這個(gè)狀態(tài),不會(huì)也無(wú)法改變。

基礎(chǔ)用法

let promise = new Promise(function(resolve, reject) {
    resolve('successed');
})

promise.then(function(data) {
    console.log(data);
}, function(err) {
    console.log(err);
})
//'successed'

Promise的構(gòu)造函數(shù)接收一個(gè)帶有兩個(gè)參數(shù)的函數(shù),這兩個(gè)參數(shù)分別是Promise的狀態(tài)改變?yōu)槌晒褪〉暮瘮?shù),然后使用then方法分別設(shè)置狀態(tài)變?yōu)槌晒褪『蟮牟僮鳌?/p>

resolve和reject函數(shù)都可以接受一個(gè)參數(shù),這個(gè)參數(shù)傳遞會(huì)給then方法,所以上面代碼,當(dāng)resolve函數(shù)把promise的狀態(tài)變成已成功之后,會(huì)把它的參數(shù)“successed”傳遞給then方法,因此最后then方法會(huì)把這個(gè)字符串打印出來(lái)。

下面我們?cè)賹?xiě)一個(gè)讀取文件的Promise

//ES5的寫(xiě)法
var fs = require('fs');

fs.readFile('./a.txt', function(err, data) {
    console.log(data.toString('utf8'));
    fs.readFile('./b.txt', function(err, data) {
        console.log(data.toString());
        fs.readFile('./c.txt', function(err, data) {
            console.log(data.toString());
        });
    });
})

//ES6的寫(xiě)法
function returnFilePromise(fileName) {
    return promise = new Promise((resolve, reject) => {
        fs.readFile(fileName, function(err, data) {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    })
}

let promise = returnFilePromise('./a.txt');

promise
    .then(function(data) {
        console.log(data.toString());
        return returnFilePromise('./b.txt');
    })
    .then(function (data) {
        console.log(data.toString());
        return returnFilePromise('./c.txt');
    })
    .then(function (data) {
        console.log(data.toString());
    })
    .catch(function (err) {
        console.log(err);
    })

上面兩段代碼的功能一樣,都是繼發(fā)異步讀取三個(gè)文件,但是我們可以看到,用回調(diào)函數(shù)來(lái)寫(xiě),則會(huì)出現(xiàn)多層嵌套,使得代碼很難看,要是嵌套層數(shù)更多的話,代碼的可讀性和可維護(hù)性就會(huì)變得很差。而Promise卻可以采用鏈?zhǔn)綄?xiě)法(因?yàn)閠hen可以返回一個(gè)新的Promise),整個(gè)過(guò)程非常直觀,最后的一個(gè)catch方法可以捕捉到前面三個(gè)then方法拋出的錯(cuò)誤。

當(dāng)然,Promise的魅力遠(yuǎn)不止這些,它還有很多有趣的方法比如finally方法、all方法和race方法等等。

Generator

基礎(chǔ)用法

Generator函數(shù)是ES6中另一種異步編程方式,它會(huì)返回一個(gè)遍歷器對(duì)象。Generator有三個(gè)跟其他函數(shù)不一樣的地方:函數(shù)名帶星號(hào)(*),函數(shù)內(nèi)部的yield表達(dá)式和next調(diào)用函數(shù)方法。

function * g () {
    console.log(0);
    yield 1;
    yield 2;
    console.log(666);
    yield 3;
    return 4;
    yield 5;
}
var ge = g();
ge.next();
ge.next();
ge.next();
ge.next();
ge.next();

//0
//{ value: 1, done: false }
//{ value: 2, done: false }
//666
//{ value: 3, done: false }
//{ value: 4, done: false }
//{ value: undefined, done: true }

由上面的代碼可以清楚地看到,我們每調(diào)用一次遍歷器對(duì)象next方法,函數(shù)就會(huì)執(zhí)行一次,直到遇到并執(zhí)行完一個(gè)yield表達(dá)式,函數(shù)就會(huì)暫停下來(lái),并保留這個(gè)狀態(tài),再調(diào)用一次next方法,函數(shù)會(huì)從上次停下來(lái)的地方接著執(zhí)行下去,直到執(zhí)行一個(gè)yield表達(dá)式后,又停下來(lái),等待下一個(gè)next方法。當(dāng)遇到return時(shí),整個(gè)函數(shù)執(zhí)行完畢,即使后面還有yield表達(dá)式也不會(huì)執(zhí)行。

next方法返回的是一個(gè)有兩個(gè)屬性的對(duì)象,第一個(gè)屬性是yield后面的值,第二個(gè)屬性是表示Generator函數(shù)是否執(zhí)行完畢,當(dāng)done屬性為true是則表示函數(shù)已經(jīng)執(zhí)行完畢了。

next方法可以帶一個(gè)參數(shù),這個(gè)參數(shù)代表上一個(gè)yield的返回值

//不帶參數(shù)
function * g () {
    var a = yield 1;
    console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next())

//{ value: 1, done: false }
//undefined
//{ value: undefined, done: true }

//帶參數(shù)
function * g () {
    var a = yield 1;
    console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next(1))

//{ value: 1, done: false }
//1
//{ value: undefined, done: true }

如果next不帶參數(shù)的話,那么上一個(gè)yield的返回值是undefined。

有了next帶參數(shù)這個(gè)功能,我們就可以在函數(shù)運(yùn)行之后,還能向函數(shù)體內(nèi)輸入值,用以調(diào)整函數(shù)行為。

當(dāng)yield太多時(shí),手動(dòng)next未免太麻煩,這時(shí)可以用for of循環(huán)來(lái)一次性執(zhí)行

function* g() {
  yield 1;
  yield 2;
  yield 3;
}

for (let n of g()) {
  console.log(n);
}
// 1 2 3 

Generator的異步使用

先簡(jiǎn)單介紹一下協(xié)程。多個(gè)函數(shù)并行執(zhí)行,但是只能有一個(gè)函數(shù)處于運(yùn)行狀態(tài),其它函數(shù)處于暫停狀態(tài),各個(gè)函數(shù)之間可以交換函數(shù)執(zhí)行權(quán)。從內(nèi)存上看,協(xié)程有多個(gè)函數(shù)棧,但是只有一個(gè)棧處于運(yùn)行狀態(tài),這是一種以犧牲內(nèi)存來(lái)實(shí)現(xiàn)多任務(wù)并行的方法。也正是因?yàn)镚enerator可以交出和收回執(zhí)行權(quán),所以它很適合處理異步

let returnFilePromise = (fileName) => {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, function(err, data) {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        })
    })
}

function * readFiles () {
    yield returnFilePromise('./a.txt');
    yield returnFilePromise('./b.txt');
    yield returnFilePromise('./c.txt');
}

let ge = readFiles();

function gogogo (fn) {
    let result = fn.next()
    if (!result.done) {
        result.value.then(function (data) {
            gogogo(fn);
            console.log(data.toString())
        }) 
    } 
}

gogogo(ge);

//This is A
//This is B
//This is C

上面依舊是一個(gè)讀取三個(gè)文件的異步代碼(讀取完a.txt后再讀取b.txt),用Generator寫(xiě)起來(lái)的感覺(jué)特別像是在寫(xiě)同步代碼,實(shí)際上他是異步執(zhí)行的,這里我們另外寫(xiě)了一個(gè)讓Generator可以自動(dòng)執(zhí)行的函數(shù),每個(gè)yield返回值都是一個(gè)Promise,實(shí)際上,Generator經(jīng)常會(huì)使用到Promise。

為了便于管理Generator函數(shù),除了使每個(gè)yield函數(shù)返回Promise外,我們也可以利用Thunk函數(shù)做到這一點(diǎn)。

Thunk函數(shù)

一般來(lái)說(shuō),Thunk函數(shù)是一種“傳名調(diào)用”的實(shí)現(xiàn)方法

var a = 1

function fn (n) {
    console.log(n);
}
fn(a + 1); //相當(dāng)于fn(2), 參數(shù)在進(jìn)入函數(shù)體之前求值,傳值調(diào)用

//使用Thunk函數(shù)
function fn (th) {
    console.log(th());
}
function thunk () {
    return a + 1;
}

fn(thunk) //傳名調(diào)用

在JS中,Thunk函數(shù)會(huì)用來(lái)把一個(gè)多參數(shù)的函數(shù)變成值接受一個(gè)回調(diào)函數(shù)為參數(shù)的單參數(shù)函數(shù),任何有回調(diào)函數(shù)作參數(shù)的函數(shù)都可以寫(xiě)成Thunk函數(shù)的形式。

let thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  }
}

在實(shí)際生產(chǎn)開(kāi)發(fā)時(shí),一般會(huì)用thunkify模塊來(lái)進(jìn)行轉(zhuǎn)化,相關(guān)模塊的具體使用這里不再贅述。

async函數(shù)

async函數(shù)是Generator的語(yǔ)法糖,使得異步操作變得更加方便和語(yǔ)義化。并且它可以跟普通函數(shù)一樣調(diào)用執(zhí)行(自帶執(zhí)行器),而不需要像Generator那樣部署執(zhí)行器。

let myAsync = async function  () {
    let a = await returnFilePromise('./a.txt');
    let b = await returnFilePromise('./b.txt');
    let c = await returnFilePromise('./c.txt');

    console.log(a.toString());
    console.log(b.toString());
    console.log(c.toString());

    return c.toString();
}

myAsync().then(data => console.log(data.toString()))

//This is A
//This is B
//This is C

//This is C

await后面接的是Promise,當(dāng)然也可以是原始類型的值,但就變成了同步操作(轉(zhuǎn)化成Promise后立即執(zhí)行resolve())。整個(gè)async函數(shù)的返回值是一個(gè)Promise,因此可以在函數(shù)調(diào)用后使用then方法,函數(shù)體內(nèi)的return的返回值會(huì)當(dāng)做then的回調(diào)函數(shù)的參數(shù)。

await會(huì)按代碼書(shū)寫(xiě)的順序執(zhí)行,在上面代碼中,必須等a.txt讀取完畢,下一個(gè)await才會(huì)執(zhí)行,也就是說(shuō),在await函數(shù)體內(nèi),await表達(dá)式是同步的。但是整個(gè)async函數(shù)是異步的。如果多個(gè)異步操作之間不存在繼發(fā)關(guān)系,這樣的等待未免多余,我們可以用Promise.all方法把多個(gè)異步操作放在一起。

await的返回值是它后面Promise中resolve或者reject的參數(shù),其中reject的參數(shù)會(huì)被catch方法捕獲,不管有沒(méi)有寫(xiě)return。只要有一個(gè)await的Promise的狀態(tài)變?yōu)閞eject,那么整個(gè)async函數(shù)就會(huì)中斷執(zhí)行(async返回的Promise的狀態(tài)變?yōu)閞eject),如果我們希望有一個(gè)異步操作的成功與否不影響其他操作,那么我們可以把它放在try···catch中。

async function getErr() {
    try {
        await Promise.reject("error");
    } catch (e) {
        console.log(e);
    }
    return await returnFilePromise('./a.txt');
}

getErr().then(data => console.log(data.toString()))

//error
//This is A

或者稍微暴力一點(diǎn),直接在那個(gè)Promise后面接一個(gè)catch方法來(lái)捕獲錯(cuò)誤

async function getErr() {
    await Promise.reject("error")
        .catch(e => console.log(e));
    return await returnFilePromise('./a.txt');
}

getErr().then(data => console.log(data.toString()));

最后

Promise確實(shí)是個(gè)好東西,Generator和async函數(shù)都用到了它。雖然在ES6之前就已經(jīng)有了Promise,但是直到ES6才被寫(xiě)進(jìn)標(biāo)準(zhǔn),并大放異彩。

還有一件事,我偷懶了沒(méi)好好寫(xiě)前兩部分的錯(cuò)誤處理,哈哈哈哈哈哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對(duì)JavaScript語(yǔ)言太重要。Javascript語(yǔ)言的執(zhí)行環(huán)境是“單線程”的,如果沒(méi)有異步編程,根本...
    呼呼哥閱讀 7,410評(píng)論 5 22
  • 首先這篇文章是參考阮一峰老師的ES6章節(jié)中的generator部分寫(xiě)的,再加上我自己的理解,新手如果要學(xué)習(xí)ES6推...
    c菜鳥(niǎo)閱讀 612評(píng)論 0 0
  • async 函數(shù) 含義 ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便。 async 函數(shù)是...
    huilegezai閱讀 1,324評(píng)論 0 6
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 6,460評(píng)論 9 19
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,891評(píng)論 0 5

友情鏈接更多精彩內(nèi)容