深入淺出javascript異步編程

原文:https://yunchi.dev/posts/demystifying-async/

從 callback 到 promise 到 generator绅络,再到 asyc/await,Javascript 中的異步編程經(jīng)歷了一系列演變蠢甲。盡管每次變化都為那些深陷 Javascript 異步編程泥潭的人們提供了一些便利司抱,但它也讓我們對(duì)每種方式的工作原理以及它們?cè)趹?yīng)用時(shí)的細(xì)微差別的理解變得很難原朝。

本次 codelab 的目標(biāo)是通過(guò)回顧 callbacks 和promises 的用法燎竖、快速介紹 generators 址芯,然后對(duì) generators 和 async/await 異步編程的工作原理提供一個(gè)直觀(guān)的了解彰居,最終讓你能夠得心應(yīng)手的在不同的場(chǎng)景下應(yīng)用合適的編程方式诚纸。

本文假設(shè)您已經(jīng)之前的異步編程中使用過(guò)callbacks,promises和generators陈惰,并且對(duì)于Javascript閉包和柯里化(currying)相當(dāng)熟悉畦徘。

回調(diào)地獄

回調(diào)就是一切的開(kāi)始。由于事件循環(huán)抬闯,強(qiáng)烈建議不要在Javascript中執(zhí)行同步I / O或阻塞井辆,因此,為了執(zhí)行任何類(lèi)型的I / O或推遲執(zhí)行任何操作溶握,異步代碼運(yùn)行的策略是傳入一個(gè)函數(shù)以便稍后在事件循環(huán)中的某個(gè)地方觸發(fā)時(shí)進(jìn)行調(diào)用杯缺。一個(gè)回調(diào)并不算多壞,但是隨著代碼量的增長(zhǎng)睡榆,并且回調(diào)通常會(huì)導(dǎo)致越來(lái)越多的回調(diào)調(diào)用其他回調(diào)萍肆。最終看起來(lái)像這樣:

getUserData(function doStuff(e, a) {
  getMoreUserData(function doMoreStuff(e, b) {
    getEvenMoreUserData(function doEvenMoreStuff(e, c) {
      getYetMoreUserData(function doYetMoreStuff(e, c) {
        console.log('Welcome to callback hell!');
      });
    });
  });
})

除了看著不斷嵌套的代碼起一身雞皮疙瘩袍榆,您還要把對(duì) do*Stuff 邏輯的控制權(quán)交給了其他函數(shù)(get*UserData()),而這些函數(shù)可能有也可能沒(méi)有源碼塘揣,您也無(wú)法真正分辨它們是否已經(jīng)調(diào)用過(guò)您的回調(diào)包雀。是不是很好?

Promises

Promises 取得了對(duì)回調(diào)提供的反轉(zhuǎn)控制亲铡,讓您將回調(diào)地獄轉(zhuǎn)變?yōu)橐粭l鏈?zhǔn)酱a才写。我們可以將最后一個(gè)示例轉(zhuǎn)換為下面的代碼:

getUserData()
  .then(getUserData)
  .then(doMoreStuff)
  .then(getEvenMoreUserData)
  .then(doEvenMoreStuff)
  .then(getYetMoreUserData)
  .then(doYetMoreStuff);

是不是看上去很簡(jiǎn)陋?可是等等=甭赞草!讓我們看一個(gè)更實(shí)際(但仍然很虛構(gòu))的回調(diào)示例:

// Suppose that we have a method fetchJson() that does GET requests and has an interface that looks
// like this, where callback is expected to take error as its first argument and the parsed response
// data as its second.
function fetchJson(url, callback) { ... }
fetchJson('/api/user/self', function(e, user) {
  fetchJson('/api/interests?userId=' + user.id, function(e, interests) {
    var recommendations = [];
    interests.forEach(function () {
      fetchJson('/api/recommendations?topic=' + interest, function(e, recommendation) {
        recommendations.push(recommendation);
        if (recommendations.length == interests.length) {
          render(profile, interests, recommendations);
        }
      });
    });
  });
});

因此,我們先獲取到用戶(hù)的個(gè)人資料锭硼,然后獲取用戶(hù)的興趣房资,然后根據(jù)用戶(hù)的興趣獲取推薦內(nèi)容,最后在獲取到所有的推薦內(nèi)容時(shí)展現(xiàn)在頁(yè)面上檀头。盡管它一系列的callbacks讓人毛骨悚然轰异,您一定會(huì)為之感到自豪,但是promise會(huì)使這一切變得更好暑始,不是嗎搭独?

讓我們?cè)囋囉胮romise替代callback來(lái)改造fetchedJson(),promise處理了格式為JSON的響應(yīng)內(nèi)容到下一個(gè)執(zhí)行方法中廊镜。

fetchJson('/api/user/self')
    .then(function (user) {
        return fetchJson('/api/user/interests?userId=' + self.id);
    })
    .then(function (interests) {
        return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))];
    })
    .then(function (recommendations) {
        render(user, interests, recommendations);
    });

是不是看上去美多了牙肝?但是讓我們看看代碼有沒(méi)有什么問(wèn)題?

糟糕嗤朴!我們?cè)阪溨凶詈笠粋€(gè)函數(shù)里無(wú)法獲取到個(gè)人資料和興趣配椭,因此這段代碼不起作用!那我們可以做什么呢雹姊?好吧股缸,我們可以再嵌套promise:

fetchJson('/api/user/self')
    .then(function (user) {
      return fetchJson('/api/user/interests?userId=' + self.id)
          .then(interests => {
            user: user,
            interests: interests
          });
    })
    .then(function (blob) {
      return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))]
          .then(recommendations => {
            user: blob.user,
            interests: blob.interests,
            recommendations: recommendations
          });
    })
    .then(function (bigBlob) {
      render(bigBlob.user, bigBlob.interests, bigBlob.recommendations);
    });

好吧……現(xiàn)在這比我們期望的要丑陋得多,這種嵌套瘋狂不是我們想要擺脫回調(diào)地獄的原因之一嗎吱雏?現(xiàn)在怎么辦敦姻?

實(shí)際上,我們可以通過(guò)利用閉包來(lái)使代碼更優(yōu)美:

// We declare these variables we want to save ahead of time.
var user, recommendations;
fetchJson('/api/user/self')
    .then(function (fetchedUser) {
      user = fetchedUser;
      return fetchJson('/api/user/interests?userId=' + self.id);
    })
    .then(function (fetchedInterests) {
      interests = fetchedInterests;
      return Promise.all(interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
    })
    .then(function (recomendations) {
      render(user, interests, recommendations);
    })
    .then(function () {
      console.log('We are done!');
    });

好吧歧杏,這幾乎和我們想要的一樣好镰惦,除了有點(diǎn)古怪。如果你注意到我們?cè)趐romise的callback中調(diào)用的參數(shù)是fetchedUser 和 fetchedInterests犬绒,而不是user 和 interests旺入,那證明你的觀(guān)察很仔細(xì)。

這種方法的缺陷在于凯力,您必須要注意茵瘾,您在內(nèi)部函數(shù)中命名的變量名稱(chēng)不能和即將在閉包中使用的“緩存”變量的名稱(chēng)相同急膀。如果您命名了一個(gè)相同名稱(chēng)的變量,那么在閉包中“緩存”變量就獲取不到龄捡。即使您盡量足夠小心以免產(chǎn)生影響卓嫂,但是在閉包中引用變量還是件很危險(xiǎn)和棘手的事。

異步Generators

Generators來(lái)拯救你了聘殖。如果使用generators晨雳,我們可以消除所有麻煩。相信我奸腺,這是真的很神奇餐禁,讓我們來(lái)看一看:

co(function* () {
  var user = yield fetchJson('/api/user/self');
  var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = yield Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
});

就是這樣,而且能夠運(yùn)行突照。您是否對(duì)generators的美麗感到不高興帮非,而且對(duì)在javascript擁有g(shù)enerators之前自己實(shí)際上是很魯莽地學(xué)習(xí)了Javascript而感到遺憾呢?我知道我曾經(jīng)做過(guò)讹蘑。

但是……這一切如何運(yùn)作的末盔?這真的是魔術(shù)嗎?當(dāng)然不是座慰。讓我們破滅這種幻想陨舱。

Generators

在我們的示例中,Generators看起來(lái)很容易使用版仔,但是實(shí)際上有很多事情要做游盲。為了深入研究異步generators,我們需要更好地了解generators的行為以及它如何能夠擁有同步外觀(guān)的異步操作蛮粮。

好吧益缎,一個(gè)generator會(huì)產(chǎn)生一系列值:

function* counts(start) {
  yield start + 1;
  yield start + 2;
  yield start + 3;
  return start + 4;
}
const counter = counts(0);
console.log(counter.next()); // {value: 1, done: false}
console.log(counter.next()); // {value: 2, done: false}
console.log(counter.next()); // {value: 3, done: false}
console.log(counter.next()); // {value: 4, done: true}
console.log(counter.next()); // {value: undefined, done: true}

這非常直接,但是讓我們來(lái)說(shuō)一下到底發(fā)生了什么:

  1. const counter = counts(0);-初始化generator并將其保存到 counter變量中然想。generator處于掛起狀態(tài)莺奔,并且generator內(nèi)部的代碼尚未執(zhí)行。
  2. console.log(counter.next());-計(jì)算出yield 1又沾,并將1作為value返回弊仪,done是false熙卡,因?yàn)檫€有更多的yield 要完成杖刷。
  3. console.log(counter.next()); -下一個(gè)是2。
  4. console.log(counter.next());-下一個(gè)是3〔蛋現(xiàn)在我們?cè)谧詈罅嘶迹遣皇蔷徒Y(jié)束了?不颓鲜,執(zhí)行在yield3處暫停表窘,我們需要再次調(diào)用next()以完成操作典予。
  5. console.log(counter.next()); -接下來(lái)是4,并且返回而不是yielded乐严,所以我們退出并完成操作瘤袖。
  6. console.log(counter.next());-生成器已經(jīng)完成!除了done之外昂验,沒(méi)有其他的可以返回捂敌。

我們現(xiàn)在了解generators如何工作的,但是請(qǐng)稍等既琴,還有個(gè)令人震驚的真相:generators不僅僅會(huì)吐出values占婉,也能夠吞掉values!

function* printer() {
  console.log("We are starting!");
  console.log(yield);
  console.log(yield);
  console.log(yield);
  console.log("We are done!");
}
const counter = printer();
counter.next(1); // We are starting!
counter.next(2); // 2
counter.next(3); // 3
counter.next(4); // 4\n We are done!
counter.next(5); // <doesn't print anything>

哇甫恩,什么逆济?!generator正在消費(fèi)values而不是生成它們磺箕。這怎么可能奖慌?

但是這個(gè)秘密就在于next函數(shù),它不僅從generator返回values松靡,而且可以將values發(fā)送回generator升薯。當(dāng)next()給定一個(gè)參數(shù)時(shí),generator當(dāng)前正在等待的yield實(shí)際上被認(rèn)為是一個(gè)參數(shù)击困。這就是為什么第一次調(diào)用counter.next(1) 記錄是undefined涎劈,此時(shí)還沒(méi)有yield被resolve。

如果generators讓調(diào)用者代碼(routine)和generator代碼(routine)作為搭檔一起運(yùn)行阅茶,它們會(huì)在執(zhí)行并彼此等待時(shí)互相來(lái)回傳遞 values蛛枚。這就好像Javascript中的generators是為您設(shè)計(jì)的能夠?qū)崿F(xiàn)合作并發(fā)執(zhí)行的routines或“co-routines”。這難道不是看起來(lái)有點(diǎn)像 co()嗎脸哀?

但是蹦浦,讓我們不要停止學(xué)習(xí),超越自己撞蜂。這項(xiàng)示例的目的是建立關(guān)于generators和異步編程的感觀(guān)盲镶,還有什么比寫(xiě)一個(gè)generator更好的方式來(lái)建立對(duì)于generators的感受?不需要編寫(xiě)或使用任何generator函數(shù)蝌诡,而是要實(shí)現(xiàn)generator函數(shù)的內(nèi)部代碼溉贿。

Generator內(nèi)部 —生成generators

好吧,我實(shí)際上并不知道在各種JS運(yùn)行時(shí)中浦旱,generator的內(nèi)部是什么樣的宇色。但是這并不重要。Generators遵循一個(gè)接口,通過(guò)一個(gè)“constructor”實(shí)例化一個(gè)generator宣蠕,提供了next(value? : any) 方法來(lái)告訴generator繼續(xù)執(zhí)行并且提供values例隆,以及拋出異常的throw(error)方法,而不是我們會(huì)忽視的一個(gè)value和return()方法抢蚀。如果我們能夠滿(mǎn)足接口要求镀层,那很好。

因此皿曲,讓我們根據(jù)上面的描述嘗試實(shí)現(xiàn)一個(gè)counts() generator鹿响,然后使用不帶function*關(guān)鍵字的ES5來(lái)編寫(xiě)。因?yàn)樗鼪](méi)有采取任何輸入谷饿,我們可以忽略throw()并傳遞當(dāng)前value 到next()里惶我。那我們?cè)撛趺醋瞿兀?/p>

好吧,實(shí)際上還有另一種暫停和繼續(xù)執(zhí)行Javascript程序的方法:閉包博投!這看起來(lái)是不是很熟悉绸贡?

function makeCounter() {
  var count = 1;
  return function () {
    return count++;
  }
}
var counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

如果您以前使用過(guò)閉包,那么我可以肯定您在過(guò)去寫(xiě)過(guò)類(lèi)似的東西毅哗。函數(shù)makeCounter執(zhí)行后的返回值是一個(gè)函數(shù)听怕,就像一個(gè)generator一樣可以生成無(wú)限個(gè)數(shù)字。

但是虑绵,此函數(shù)不遵守generator接口尿瞭,也不直接適用于我們的counts()示例,返回了4個(gè)值并結(jié)束翅睛。我們?nèi)绾瓮ㄟ^(guò)一個(gè)通用方法來(lái)實(shí)現(xiàn)類(lèi)似generator的函數(shù)呢声搁?

閉包,狀態(tài)機(jī)以及繁重的工作捕发!

function counts(start) {
  let state = 0;
  let done = false;
  function go() {
    let result;
    switch (state) {
      case 0:
        result = start + 1;
        state = 1;
        break;
      case 1:
        result = start + 2;
        state = 2;
        break;
      case 2:
        result = start + 3;
        state = 3;
        break;
      case 3:
        result = start + 4;
        done = true;
        state = -1;
        break;
      default:
        break;
    }
    return {done: done, value: result};
  }
  return {
    next: go
  }
}
const counter = counts(0);
console.log(counter.next()); // {value: 1, done: false}
console.log(counter.next()); // {value: 2, done: false}
console.log(counter.next()); // {value: 3, done: false}
console.log(counter.next()); // {value: 4, done: true}
console.log(counter.next()); // {value: undefined, done: true}

如果運(yùn)行這段代碼疏旨,您將看到我們得到與generator版本相同的結(jié)果。整潔吧扎酷?

這是一個(gè)簡(jiǎn)單的狀態(tài)機(jī)檐涝,可以通過(guò)調(diào)用.next()可以轉(zhuǎn)換每個(gè)yield 語(yǔ)句的狀態(tài)。把它繪制成圖表法挨,看起來(lái)像這樣:


state-machine-counts.png

好的谁榜,既然我們已經(jīng)解構(gòu)了generator-as-producer,那么我們?nèi)绾稳?shí)現(xiàn)generator-as-consumer呢凡纳?

實(shí)際上沒(méi)有太大區(qū)別窃植。

function printer(start) {
  let state = 0;
  let done = false;
  function go(input) {
    let result;
    switch (state) {
      case 0:
        console.log("We are starting!");
        state = 1;
        break;
      case 1:
        console.log(input);
        state = 2;
        break;
      case 2:
        console.log(input);
        state = 3;
        break;
      case 3:
        console.log(input);
        console.log("We are done!");
        done = true;
        state = -1;
        break;
      default:
        break;
      return {done: done, value: result};
    }
  }
  return {
    next: go
  }
}
const counter = printer();
counter.next(1); // We are starting!
counter.next(2); // 2
counter.next(3); // 3
counter.next(4); // 4
counter.next(5); // We are done!
state-machine-printer.png

我們要做的就是將輸入作為參數(shù)來(lái)運(yùn)行,并且通過(guò)管道傳遞值惫企。有點(diǎn)神奇吧撕瞧?幾乎像generators一樣神奇。

好極了狞尔!現(xiàn)在丛版,我們已經(jīng)實(shí)現(xiàn)了一個(gè)generator-as-producer和一個(gè)enerator-as-consumer。我們?yōu)槭裁床辉囋噷?shí)現(xiàn)一個(gè)generator-as-producer-and-consumer呢偏序?

這是另一個(gè)人造的 generator:

function* adder(initialValue) {
  let sum = initialValue;
  while (true) {
    sum += yield sum;
  }
}

由于我們現(xiàn)在是generator專(zhuān)家页畦,因此我們了解到此generator將next(value)中傳入的值加到sum上,并返回得到的sum研儒。它的行為就像我們期望的那樣:

const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.next(3)); // 6

很酷≡ビВ現(xiàn)在,讓我們將這個(gè)接口用常規(guī)函數(shù)來(lái)實(shí)現(xiàn)端朵!

function adder(initialValue) {
  let state = 'initial';
  let done = false;
  let sum = initialValue;
  function go(input) {
    let result;
    switch (state) {
      case 'initial':
        result = initialValue;
        state = 'loop';
        break;
      case 'loop':
        sum += input;
        result = sum;
        state = 'loop';
        break;
      default:
        break;
    }
    return {done: done, value: result};
  }
  return {
    next: go
  }
}
function runner() {
  const add = adder(0);
  console.log(add.next()); // 0
  console.log(add.next(1)); // 1
  console.log(add.next(2)); // 3
  console.log(add.next(3)); // 6
}
runner();

我們已經(jīng)實(shí)現(xiàn)了一個(gè)真正的協(xié)例好芭。runner() 給adder()傳入了值,adder()相加并返回總和冲呢,然后 runner()打印sum值并給出一個(gè)新的value讓adder()來(lái)加舍败。

關(guān)于generators,我們還有更多內(nèi)容需要討論敬拓。異常如何工作邻薯?好吧,generator拋出的異常非常容易:該異常從next()冒泡至調(diào)用者乘凸,一直到generator結(jié)束厕诡。另一個(gè)方向,將異常從調(diào)用者至generator的傳遞营勤,取決于我們隱藏的throw() 方法灵嫌。

為了演示這一點(diǎn),讓我們?yōu)閍dder提供一個(gè)瘋狂的新功能葛作。如果調(diào)用者 .throw() 拋出一個(gè)異常到generator醒第,則generator會(huì)將sum作為返回的最后一個(gè)值。實(shí)現(xiàn)起來(lái)看起來(lái)就像這樣:

function* adder(initialValue) {
  let sum = initialValue;
  let lastSum = initialValue;
  let temp;
  while (true) {
    try {
      temp = sum;
      sum += yield sum;
      lastSum = temp;
    } catch (e) {
      sum = lastSum;
    }
  }
}
const add = adder(0);
console.log(add.next()); // 0
console.log(add.next(1)); // 1
console.log(add.next(2)); // 3
console.log(add.throw(new Error('BOO)!'))); // 1
console.log(add.next(4)); // 5
state-machine-adder.png

Generator內(nèi)部 — 錯(cuò)誤傳播

Oh boy进鸠,我們?nèi)绾味xthrow()稠曼?

簡(jiǎn)單!錯(cuò)誤只是另一個(gè)值客年。我們可以將其作為另一個(gè)參數(shù)傳遞給go()霞幅。請(qǐng)注意,我們實(shí)際上必須在這里稍加小心量瓜。當(dāng) throw(e)被調(diào)用時(shí)司恳,generator內(nèi)部的yield將如同其寫(xiě)成throw e一樣的效果。這意味著我們實(shí)際上應(yīng)該檢查狀態(tài)機(jī)中每個(gè)狀態(tài)的錯(cuò)誤绍傲,一旦無(wú)法處理扔傅,就會(huì)拋出異常耍共。

在以前的adder實(shí)現(xiàn)的基礎(chǔ)上,我們必須添加一些錯(cuò)誤檢查和try/catch塊猎塞。

function adder(initialValue) {
  let state = 'initial';
  let done = false;
  let sum = initialValue;
  let lastSum;
  let temp;

  function go(input, err) {
    let result;

    switch (state) {
      case 'initial':
        if (err) {
          throw err;
        }
        temp = sum;
        result = initialValue;
        state = 'loop';
        break;
      case 'loop':
        try {
          if (err) {
            throw err;
          }
          sum += input;
          lastSum = temp;
          temp = sum;
        } catch (e) {
          sum = lastSum;
        }
        result = sum;
        state = 'loop';
        break;
      default:
        break;
    }

    return {done: done, value: result};
  }

  return {
    next: go,
    throw: function (err) {
      return go(undefined, err)
    }
  }
}

function runner() {
  const add = adder(0);
  console.log(add.next()); // 0
  console.log(add.next(1)); // 1
  console.log(add.next(2)); // 3
  console.log(add.throw(new Error('BOO)!'))); // 1
  console.log(add.next(4)); // 5
}

runner();

完美试读!我們已經(jīng)實(shí)現(xiàn)了一組協(xié)同例程,它們可以像真正的生成器一樣互相傳遞消息和異常荠耽。

但這實(shí)際上變得相當(dāng)復(fù)雜钩骇,這只是說(shuō)明了generators在幕后為我們做了多少事。我們使用generators免費(fèi)提供給我們的閉包就可以手動(dòng)做很多事情铝量。

  • 當(dāng)閉包的狀態(tài)可以?huà)炱饡r(shí)倘屹,錯(cuò)誤傳播和處理的執(zhí)行很復(fù)雜
  • 實(shí)際上,為了正確地暫停循環(huán)慢叨,Loops必須以笨拙的方式“unrolled”纽匙。我們的loop示例實(shí)際上是運(yùn)行while循環(huán)的后半部分,而運(yùn)行前半部分拍谐,以匹配generator中的yield哄辣。

我們不必自己實(shí)現(xiàn)generators是一件好事情。

你做到了T病力穗!我們已經(jīng)深入探討了generators的潛在實(shí)現(xiàn)方式,并希望您對(duì)發(fā)generators的工作方式有了更直觀(guān)的了解气嫁。綜上所述:

  • generators可以產(chǎn)生或消費(fèi)值当窗,或兩者都有
  • generators的狀態(tài)可以暫停(狀態(tài),狀態(tài)機(jī)寸宵,明白崖面?)
  • 一個(gè)調(diào)用者和一個(gè)generators可以形成一組相互協(xié)作的協(xié)同例程
  • 異常可以向任一方向發(fā)送梯影。

現(xiàn)在巫员,我們有了一個(gè)更好的理解,一種對(duì)generators潛在有用的思考是我們編寫(xiě)并發(fā)運(yùn)行的例程的語(yǔ)法甲棍,而該例程可以通過(guò)單個(gè)值通道(yield語(yǔ)句)來(lái)相互傳遞消息 简识。在下一部分中,這將很有用感猛,我們的co()將從協(xié)例程中派生實(shí)現(xiàn)七扰。

使用協(xié)程進(jìn)行控制反轉(zhuǎn)

現(xiàn)在我們是generator專(zhuān)家,讓我們考慮如何將generators應(yīng)用于異步編程中來(lái)陪白。能夠自己編寫(xiě)generators并不意味著generators內(nèi)的Promise會(huì)自動(dòng)得到resolved颈走。但是,等等咱士,generators并不是設(shè)計(jì)成自己工作立由。它們被設(shè)計(jì)為與另一個(gè)調(diào)用.next()和.throw()的程序(主程序)一起工作轧钓。

如果不是將業(yè)務(wù)邏輯放在主要例程中,而是將所有業(yè)務(wù)邏輯放在generator中锐膜,該怎么辦毕箍?每次業(yè)務(wù)邏輯遇到諸如Promise之類(lèi)的異步值時(shí),generator都會(huì)發(fā)出這樣的消息:“我不想處理這種在解決時(shí)就喚醒我的瘋狂”枣耀,然后暫停并將Promise扔給服務(wù)例程霉晕,同時(shí)服務(wù)例程決定:“很好庭再,我稍后再調(diào)用您”飒箭。然后木蹬,服務(wù)例程會(huì)在Promise上注冊(cè)一個(gè)callback,然后退出,當(dāng)Promise解析時(shí)等待事件循環(huán)調(diào)用它车胡。完成后,它會(huì)“嘿蝗茁,準(zhǔn)備好了娜汁,輪到您了”,然后通過(guò).next()發(fā)送值進(jìn)入休眠的generator斧抱,等待generator執(zhí)行其操作常拓,然后返回另一個(gè)異步的事項(xiàng)來(lái)處理……等等之類(lèi)的工作。所以關(guān)于服務(wù)例程如何為generator始終提供服務(wù)的是一個(gè)令人悲傷的故事辉浦。

現(xiàn)在讓我們回到主要主題弄抬。鑒于我們對(duì)generators和promises如何工作的了解,對(duì)于我們來(lái)說(shuō)宪郊,創(chuàng)建這個(gè)“服務(wù)例程”應(yīng)該并不困難掂恕。服務(wù)例程本身將作為Promise同時(shí)執(zhí)行,實(shí)例化并服務(wù)于generator弛槐,然后通過(guò).then()回調(diào)將最終結(jié)果返回給我們的主例程懊亡。

現(xiàn)在讓我們回頭看看co()程序。co()是從動(dòng)的服務(wù)例程乎串,以便generator只能使用同步值來(lái)工作〉暝妫現(xiàn)在是不是變得更有意義了?

  var user = yield fetchJson('/api/user/self');
  var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = yield Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
});

對(duì)于那些熟悉蹦床函數(shù)的人叹誉,co()可以將其視為一個(gè)蹦床函數(shù)的異步版本艰争,即蹦床Promises。

co()的簡(jiǎn)化版本

很好桂对!現(xiàn)在讓我們自己來(lái)實(shí)現(xiàn)co()甩卓,以建立一些關(guān)于此從屬例程工作原理的感觀(guān)。co()必須

  1. 返回一個(gè)以供調(diào)用者來(lái)等待
  2. 實(shí)例化generator
  3. 調(diào)用generator上的.next()以獲取第一個(gè) yielded 結(jié)果蕉斜,該結(jié)果的形式應(yīng)為 {done: false, value: [a Promise]}
  4. 在Promise上注冊(cè)一個(gè)callback
  5. 當(dāng)promise解析(callback被調(diào)用)后逾柿,使用resolved的值調(diào)用generator的.next()缀棍,并獲取另一個(gè)值返回
  6. 從步驟4重復(fù)
  7. 如果generator在任何時(shí)候返回 {done: true, value: ...},resolve由co()返回的promise

現(xiàn)在机错,讓我們不用擔(dān)心錯(cuò)誤爬范,來(lái)實(shí)現(xiàn)一個(gè)可以處理以下人工示例的co()方法:

function deferred(val) {
  return new Promise((resolve, reject) => resolve(val));
}

co(function* asyncAdds() {
  console.log(yield deferred(1)); // 1
  console.log(yield deferred(2)); // 2
  console.log(yield deferred(3)); // 3
  return 4;
}).then(function (result) {
  console.log(result); // 4
});

function co(generator) {
  return new Promise((resolve, reject) => {
    // Your code goes here
  });
}

如果您有一臺(tái)可用的計(jì)算機(jī),我建議在繼續(xù)閱讀下面的解決方案之前弱匪,請(qǐng)先嘗試下自己實(shí)現(xiàn)該解決方案青瀑。相比只是閱讀解決方法而不去嘗試實(shí)現(xiàn),這能夠更好地幫助您更好地理解co()萧诫。

該實(shí)現(xiàn)應(yīng)最終看起來(lái)像這樣:

function co(generator) {
  return new Promise((resolve, reject) => {
    const g = generator();

    function next(nextVal) {
      const ret = g.next(nextVal);
      if (ret.done) {
        return resolve(ret.value);
      }

      ret.value.then(next);
    }
    next();
  });
}

還不錯(cuò)吧斥难?用大約10行代碼,我們實(shí)現(xiàn)了曾經(jīng)神奇和全能的核心功能co()帘饶。讓我們看看是否可以再增加點(diǎn)什么哑诊。異常處理如何?

擁有異常處理的co()

當(dāng)generator產(chǎn)生的Promise被拒絕時(shí)及刻,我們想要co()可以將異常通知給generator例程镀裤。請(qǐng)記住,generator接口為我們提供了一個(gè)發(fā)送異常的方法.throw()缴饭。

從此擁有全新的deferReject 函數(shù)的模板開(kāi)始暑劝,讓我們嘗試實(shí)現(xiàn)一個(gè)可以處理被拒絕的諾言的co()方法。

function deferred(val) {
  return new Promise((resolve, reject) => resolve(val));
}

function deferReject(e) {
  return new Promise((resolve, reject) => reject(e));
}

co(function* asyncAdds() {
  console.log(yield deferred('We are starting!'));
  try {
    console.log(yield deferReject(new Error('To fail, or to not fail.')));
  } catch (e) {
    console.log('We recovered!');
  }
  console.log(yield deferred('We finished!'));
});

function co(generator) {
  return new Promise((resolve, reject) => {
    // Your code goes here
  });
}

異常處理有些棘手颗搂。取決于產(chǎn)生的promise是否已resolved或rejected担猛,我們需要不同的callbacks,因此我們希望將next()函數(shù)的代碼拆分為單獨(dú)的onResolve()和 onReject()方法峭火。onResolve()將負(fù)責(zé)調(diào)用generator的.next()毁习,而onResolve()將負(fù)責(zé)調(diào)用.throw()。 如果generator拋出錯(cuò)誤(而不是產(chǎn)生rejected promise)卖丸,則兩個(gè)callbacks都將用try/catch包裝自身以便立即拒絕co()的promise纺且。

同樣,如果您有一臺(tái)可用的計(jì)算機(jī)稍浆,建議您先嘗試一下自己解決载碌,然后再繼續(xù)向下查看解決方案。

添加新邏輯并不像聽(tīng)起來(lái)那樣復(fù)雜衅枫,最終會(huì)變成如下所示:

function co(generator) {
  return new Promise((resolve, reject) => {
    const g = generator();

    function onResolve(value) {
      let ret;

      try {
        ret = g.next(value);
      } catch (e) {
        reject(e)
        return;
      }
      next(ret);
    }

    function onReject(err) {
      let ret;

      try {
        ret = g.throw(err);
      } catch (e) {
        reject(e);
        return;
      }
      next(ret);
    }

    function next(ret) {
      if (ret.done) {
        return resolve(ret.value);
      }

      ret.value.then(onResolve, onReject);
    }

    onResolve();
  });
}

到此嫁艇,我們已經(jīng)實(shí)現(xiàn)co()!幾乎弦撩!co()還支持thunks步咪,嵌套generators,上述數(shù)組和上述深層對(duì)象益楼。但是猾漫,現(xiàn)在是不是看上去不再那么神奇了点晴?

作為co()自己完成任務(wù)的一種額外方法,讓我們進(jìn)行實(shí)際操作(按照此鏈接)來(lái)真正完成co()悯周。是不是看起來(lái)幾乎一樣酷粒督?

圣杯:async/await

是的,現(xiàn)在我們理解了generators和co()禽翼。但這對(duì)我們使用 async/await有用嗎屠橄?答案是肯定的!到目前為止闰挡,我們已經(jīng)建立的理解讓我們很容易理解async/await锐墙。

async關(guān)鍵字允許我們可以聲明一個(gè)可以被await關(guān)鍵字暫停的函數(shù),就像generators可以用yield 關(guān)鍵字來(lái)暫停解总。await只能在Promises上使用贮匕,并且只能在被async包裹的函數(shù)的執(zhí)行堆棧中使用姐仅。async函數(shù)在執(zhí)行時(shí)返回Promises花枫。

所以要像下面使用async/await替代generators來(lái)轉(zhuǎn)換函數(shù),你基本上要用async替換co()和用await替換yield掏膏,并從函數(shù)上刪除*劳翰,因此它不再是一臺(tái)generator。

co(function* () {
  var user = yield fetchJson('/api/user/self');
  var interests = yield fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = yield Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
});

變成:

async function () {
  var user = await fetchJson('/api/user/self');
  var interests = await fetchJson('/api/user/interests?userId=' + self.id);
  var recommendations = await Promise.all(
      interests.map(i => fetchJson('/api/recommendations?topic=' + i)));
  render(user, interests, recommendations);
}();

但是有一些小怪異/差異要注意:

  • co()立即執(zhí)行異步generator馒疹。async創(chuàng)建了函數(shù)佳簸,但是您仍然必須調(diào)用它。async更像是co()變體 co.wrap()颖变。
  • 使用co()生均,您可以yield Promises,thunks腥刹,Promises 數(shù)組或Promises 對(duì)象马胧。使用async,您只能在Promises上使用await衔峰。
  • 您不能使用async到generators的await佩脊。那是沒(méi)有道理的。但是您可以co()用來(lái)將用異步generators編寫(xiě)的代碼包裝到Promise中垫卤,然后再使用await 威彰。

結(jié)語(yǔ)

我們回顧了Javascript異步編程的全部簡(jiǎn)短歷史,找出了generators和co()“幕后”的工作方式穴肘,然后了解了如何將學(xué)習(xí)到的內(nèi)容應(yīng)用于async/await歇盼。你應(yīng)該感到自豪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末评抚,一起剝皮案震驚了整個(gè)濱河市豹缀,隨后出現(xiàn)的幾起案子赵讯,更是在濱河造成了極大的恐慌,老刑警劉巖耿眉,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件边翼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鸣剪,警方通過(guò)查閱死者的電腦和手機(jī)组底,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)筐骇,“玉大人债鸡,你說(shuō)我怎么就攤上這事☆跷常” “怎么了厌均?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)告唆。 經(jīng)常有香客問(wèn)我棺弊,道長(zhǎng),這世上最難降的妖魔是什么擒悬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任模她,我火速辦了婚禮,結(jié)果婚禮上懂牧,老公的妹妹穿的比我還像新娘侈净。我一直安慰自己,他們只是感情好僧凤,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布畜侦。 她就那樣靜靜地躺著,像睡著了一般躯保。 火紅的嫁衣襯著肌膚如雪旋膳。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天吻氧,我揣著相機(jī)與錄音溺忧,去河邊找鬼。 笑死盯孙,一個(gè)胖子當(dāng)著我的面吹牛鲁森,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播振惰,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼歌溉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起痛垛,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤草慧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后匙头,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體漫谷,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蹂析,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舔示。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡电抚,死狀恐怖惕稻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝙叛,我是刑警寧澤俺祠,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站借帘,受9級(jí)特大地震影響蜘渣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姻蚓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一宋梧、第九天 我趴在偏房一處隱蔽的房頂上張望匣沼。 院中可真熱鬧狰挡,春花似錦、人聲如沸释涛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)唇撬。三九已至它匕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窖认,已是汗流浹背豫柬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扑浸,地道東北人烧给。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像喝噪,于是被迫代替她去往敵國(guó)和親础嫡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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