ES6標(biāo)準(zhǔn)入門 摘要 (async)

含義

async 函數(shù)是什么钦听?一句話洒试,它就是 Generator 函數(shù)的語法糖。

依次讀取兩個(gè)文件朴上,可以寫成async函數(shù)變得更像同步函數(shù)

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
  • async函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成async儡司,將yield替換成await,并自帶執(zhí)行器余指。
  • async和await捕犬,比起星號(hào)和yield,語義更清楚了酵镜。async表示函數(shù)里有異步操作碉碉,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。
  • co模塊約定淮韭,yield命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象垢粮,而async函數(shù)的await命令后面,可以是 Promise 對(duì)象和原始類型的值(數(shù)值靠粪、字符串和布爾值蜡吧,但這時(shí)會(huì)自動(dòng)轉(zhuǎn)成立即 resolved 的 Promise 對(duì)象
  • async函數(shù)的返回值是 Promise 對(duì)象毫蚓,這比 Generator 函數(shù)的返回值是 Iterator 對(duì)象方便多了。你可以用then方法指定下一步的操作昔善。

async函數(shù)完全可以看作多個(gè)異步操作元潘,包裝成的一個(gè) Promise 對(duì)象,而await命令就是內(nèi)部then命令的語法糖君仆。

基本用法

async函數(shù)返回一個(gè) Promise 對(duì)象翩概,可以使用then方法添加回調(diào)函數(shù)。當(dāng)函數(shù)執(zhí)行的時(shí)候返咱,一旦遇到await就會(huì)先返回钥庇,等到異步操作完成,再接著執(zhí)行函數(shù)體內(nèi)后面的語句咖摹。

多種場(chǎng)景下的寫法:

// 函數(shù)聲明
async function foo() {}

// 函數(shù)表達(dá)式
const foo = async function () {};

// 對(duì)象的方法
let obj = { async foo() {} };

// 箭頭函數(shù)
const foo = async () => {};

語法

關(guān)于async函數(shù)的返回值問題

  • async函數(shù)返回一個(gè) Promise 對(duì)象

  • async函數(shù)內(nèi)部return語句返回的值评姨,會(huì)成為返回的Promise對(duì)象的then方法回調(diào)函數(shù)的參數(shù)

  • async函數(shù)內(nèi)部拋出錯(cuò)誤,會(huì)導(dǎo)致返回的 Promise 對(duì)象變?yōu)閞eject狀態(tài)萤晴。拋出的錯(cuò)誤對(duì)象會(huì)被catch方法回調(diào)函數(shù)接收到参咙。

  • async函數(shù)返回的 Promise 對(duì)象,必須等到內(nèi)部所有await命令后面的 Promise 對(duì)象執(zhí)行完硫眯,才會(huì)發(fā)生狀態(tài)改變,除非遇到return語句或者拋出錯(cuò)誤择同。也就是說两入,只有async函數(shù)內(nèi)部的異步操作執(zhí)行完,才會(huì)執(zhí)行返回的Promise對(duì)象的then方法指定的回調(diào)函數(shù)敲才。

關(guān)于await

  • await命令只能用在async函數(shù)之中裹纳,如果用在普通函數(shù),就會(huì)報(bào)錯(cuò)

  • await命令后面是一個(gè) Promise 對(duì)象紧武,返回該對(duì)象的結(jié)果剃氧。如果不是 Promise 對(duì)象,就直接返回對(duì)應(yīng)的值阻星。

  • await命令后面是一個(gè)thenable對(duì)象(即定義then方法的對(duì)象)朋鞍,那么await會(huì)將其等同于 Promise 對(duì)象。

  • 可以通過await 完成簡(jiǎn)單的休眠的是實(shí)現(xiàn)

// 用法 每個(gè)1秒依次輸出1到5
async function fn() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await new Promise(resolve => {
      setTimeout(resolve, 1000)
    });
  }
}
  • await命令后面的 Promise 對(duì)象如果變?yōu)閞eject狀態(tài)妥箕,則reject的參數(shù)會(huì)被async函數(shù)返回的Promise實(shí)例的catch方法的回調(diào)函數(shù)接收到滥酥。
async function f() {
  await Promise.reject('出錯(cuò)了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
  • 任何一個(gè)await語句后面的 Promise 對(duì)象變?yōu)閞eject狀態(tài),那么整個(gè)async函數(shù)都會(huì)中斷執(zhí)行畦幢。如果不想前一個(gè)await 的 Promise的中斷執(zhí)行影響后面的await的異步操作坎吻,可以這么寫:
async function f() {
  try {
    await Promise.reject('出錯(cuò)了');
  } catch(e) {
    // 使用catch去捕獲錯(cuò)誤,這樣就不會(huì)影響下一個(gè)awiat Promise的執(zhí)行
    // 也不會(huì)觸發(fā)f()這個(gè)Promise實(shí)例的catch方法
    // 捕獲到錯(cuò)誤了就不會(huì)中斷執(zhí)行 也不會(huì)改變實(shí)例的狀態(tài)
  }
  return await Promise.resolve('hello world');
}

f()
.then(console.log) // 打印出 hello world
.catch(console.log) // 不會(huì)執(zhí)行


// 也可以不使用try...catch塊來捕獲錯(cuò)誤
// Promise.reject('出錯(cuò)了') 的返回值本身就是一個(gè)Promise
// 可以直接在后面鏈?zhǔn)秸{(diào)用catch方法來捕獲錯(cuò)誤宇葱,這樣也不會(huì)影響后面代碼的執(zhí)行

// 如果有多個(gè)await命令瘦真,更推薦將所有的await統(tǒng)一放在try...catch結(jié)構(gòu)中刊头。
// 這樣一個(gè)catch就能處理多個(gè)await 可能出現(xiàn)的錯(cuò)誤

下面的例子使用try...catch結(jié)構(gòu),實(shí)現(xiàn)多次重復(fù)嘗試

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      // 如果一次成功就直接跳出循環(huán)
      // 如果發(fā)生錯(cuò)誤了 就會(huì)跳過break 直接執(zhí)行catch
      // 然后再次進(jìn)入循環(huán) 再次發(fā)起請(qǐng)求
      // NUM_RETRIES 定義了這個(gè)重復(fù)嘗試的次數(shù) 
      // 如果愿意 可以設(shè)置循環(huán)條件達(dá)到直到請(qǐng)求成功再跳出循環(huán)的目的
      break;
    } catch(err) {}
  }
  console.log(i); // 3
}

test();
  • 多個(gè)await命令后面的異步操作诸尽,如果不存在繼發(fā)關(guān)系原杂,最好讓它們同時(shí)觸發(fā)。
let foo = await getFoo();
let bar = await getBar();
// 如果在async函數(shù)中這樣寫 這兩個(gè)異步操作 getBar會(huì)等到getFoo執(zhí)行完成后執(zhí)行
// 如果 getBar 與 getFoo 并不存在繼發(fā)方式弦讽,這無疑會(huì)影響性能

// 如果不存在繼發(fā)關(guān)系污尉,最好讓它們同時(shí)觸發(fā)

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();  // 這樣寫也是一樣 因?yàn)楫惒讲僮鞅煌瑫r(shí)啟動(dòng)了
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
  • async 函數(shù)可以保留運(yùn)行堆棧
const a = () => {
  var num = 1
  Promise.resolve().then(() => console.log(x)); // 報(bào)錯(cuò)
  console.log(num+1); // 2 
};
a()
// 先輸出2 再報(bào)錯(cuò) 錯(cuò)誤堆棧不包過a()

函數(shù)a內(nèi)部運(yùn)行了一個(gè)異步任務(wù)。當(dāng)異步任務(wù)運(yùn)行的時(shí)候往产,函數(shù)a()不會(huì)中斷被碗,而是繼續(xù)執(zhí)行。等到b()運(yùn)行結(jié)束仿村,可能a()早就運(yùn)行結(jié)束了锐朴,b()所在的上下文環(huán)境已經(jīng)消失了。如果b()或c()報(bào)錯(cuò)蔼囊,錯(cuò)誤堆棧將不包括a()焚志。

可以改為async函數(shù)

const a = async () => {
  await Promise.resolve().then(() => console.log(x)); // 報(bào)錯(cuò);
  console.log(num+1); // 不會(huì)執(zhí)行
};

// 直接報(bào)錯(cuò)  錯(cuò)誤堆棧包括a()

async 函數(shù)的實(shí)現(xiàn)原理

async 函數(shù)的實(shí)現(xiàn)原理,就是將 Generator 函數(shù)和自動(dòng)執(zhí)行器畏鼓,包裝在一個(gè)函數(shù)里酱酬。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  // spawn函數(shù)就是自動(dòng)執(zhí)行器
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(); });
  });
}

與其他異步處理方法的比較

async 函數(shù)的實(shí)現(xiàn)最簡(jiǎn)潔,最符合語義云矫,幾乎沒有語義不相關(guān)的代碼膳沽。它將 Generator 寫法中的自動(dòng)執(zhí)行器,改在語言層面提供让禀,不暴露給用戶挑社,因此代碼量最少。如果使用 Generator 寫法巡揍,自動(dòng)執(zhí)行器需要用戶自己提供痛阻。

假定某個(gè) DOM 元素上面,部署了一系列的動(dòng)畫腮敌,前一個(gè)動(dòng)畫結(jié)束阱当,才能開始后一個(gè)。如果當(dāng)中有一個(gè)動(dòng)畫出錯(cuò)糜工,就不再往下執(zhí)行斗这,返回上一個(gè)成功執(zhí)行的動(dòng)畫的返回值。

async function chainAnimationsAsync(elem, animations) {
 let ret = null;
 try {
   for(let anim of animations) {
     ret = await anim(elem);
   }
 } catch(e) {
   /* 忽略錯(cuò)誤啤斗,繼續(xù)執(zhí)行 */
   // 上面的await 后面的表達(dá)式發(fā)生錯(cuò)誤 就會(huì)進(jìn)入catch塊
   // 就不會(huì)繼續(xù)進(jìn)行循環(huán) 從而執(zhí)行下面的return 返回上一個(gè)成功執(zhí)行的返回值
 }
 return ret;
}

按順序完成異步操作

場(chǎng)景:依次遠(yuǎn)程讀取一組 URL表箭,然后按照讀取的順序輸出結(jié)果。

// Promise的寫法

function logInOrder(urls) {
  // 遠(yuǎn)程讀取所有URL
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });

  // 按次序輸出
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

// async 函數(shù)實(shí)現(xiàn)

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

// 上面代碼確實(shí)大大簡(jiǎn)化,問題是所有遠(yuǎn)程操作都是繼發(fā)免钻。
// 只有前一個(gè) URL 返回結(jié)果彼水,才會(huì)去讀取下一個(gè) URL

async function logInOrder(urls) {
  // 并發(fā)讀取遠(yuǎn)程URL
  // 
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

雖然map方法的參數(shù)是async函數(shù),但它是并發(fā)執(zhí)行的极舔,因?yàn)橹挥衋sync函數(shù)內(nèi)部是繼發(fā)執(zhí)行凤覆,外部不受影響。后面的for..of循環(huán)內(nèi)部使用了await拆魏,因此實(shí)現(xiàn)了按順序輸出

頂層 await

目前盯桦,有一個(gè)語法提案,允許在模塊的頂層獨(dú)立使用await命令渤刃。這個(gè)提案的目的拥峦,是借用await解決模塊異步加載的問題。

// awaiting.js
let output;
(async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };

模塊awaiting.js的輸出值output卖子,取決于異步操作略号。我們把異步操作包裝在一個(gè) async 函數(shù)里面,然后調(diào)用這個(gè)函數(shù)洋闽,只有等里面的異步操作都執(zhí)行玄柠,變量output才會(huì)有值,否則就返回undefined诫舅。

下面是加載這個(gè)模塊的寫法:

// usage.js
import { output } from "./awaiting.js";

function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100)); // NaN 
setTimeout(() => console.log(outputPlusValue(100)), 1000); // 可能會(huì)正常輸出

// outputPlusValue()的執(zhí)行結(jié)果羽利,完全取決于執(zhí)行的時(shí)間。如果awaiting.js里面的異步操作沒執(zhí)行完刊懈,加載進(jìn)來的output的值就是undefined

目前的解決方法这弧,就是讓原始模塊輸出一個(gè) Promise 對(duì)象,從這個(gè) Promise 對(duì)象判斷異步操作有沒有結(jié)束俏讹。

// awaiting.js
let output;
export default (async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };
// usage.js
import promise, { output } from "./awaiting.js";

function outputPlusValue(value) { return output + value }

promise.then(() => {
  // promise的then函數(shù)執(zhí)行的時(shí)候 說明output已經(jīng)有值了
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100)), 1000);
});

這種寫法比較麻煩,等于要求模塊的使用者遵守一個(gè)額外的使用協(xié)議畜吊,按照特殊的方法使用這個(gè)模塊泽疆。一旦你忘了要用 Promise 加載,只使用正常的加載方法玲献,依賴這個(gè)模塊的代碼就可能出錯(cuò)殉疼。而且,如果上面的usage.js又有對(duì)外的輸出捌年,等于這個(gè)依賴鏈的所有模塊都要使用 Promise 加載瓢娜。

頂層的await命令,就是為了解決這個(gè)問題礼预。它保證只有異步操作完成眠砾,模塊才會(huì)輸出值。

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);

// 兩個(gè)異步操作在輸出的時(shí)候托酸,都加上了await命令褒颈。只有等到異步操作完成柒巫,這個(gè)模塊才會(huì)輸出值。

// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代碼的寫法谷丸,與普通的模塊加載完全一樣堡掏。也就是說,模塊的使用者完全不用關(guān)心刨疼,依賴模塊的內(nèi)部有沒有異步操作泉唁,正常加載即可。

這時(shí)揩慕,模塊的加載會(huì)import的時(shí)候就會(huì)等待依賴模塊(上例是awaiting.js)的異步操作完成亭畜,才執(zhí)行后面的代碼,有點(diǎn)像暫停在那里漩绵。所以贱案,它總是會(huì)得到正確的output,不會(huì)因?yàn)榧虞d時(shí)機(jī)的不同止吐,而得到不一樣的值宝踪。

下面是頂層await的一些使用場(chǎng)景。

// import() 方法加載
const strings = await import(`/i18n/${navigator.language}`);

// 數(shù)據(jù)庫(kù)操作
const connection = await dbConnector();

// 依賴回滾
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

注意碍扔,如果加載多個(gè)包含頂層await命令的模塊瘩燥,加載命令是同步執(zhí)行的。

// x.js
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");

// y.js
console.log("Y");

// z.js
import "./x.js";
import "./y.js";
console.log("Z");

打印結(jié)果是X1不同、Y厉膀、X2、Z二拐。這說明服鹅,z.js并沒有等待x.js加載完成,再去加載y.js百新。加載命令是同步執(zhí)行的企软。

頂層的await命令有點(diǎn)像,交出代碼的執(zhí)行權(quán)給其他的模塊加載饭望,等異步操作完成后仗哨,再拿回執(zhí)行權(quán),繼續(xù)向下執(zhí)行铅辞。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厌漂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子斟珊,更是在濱河造成了極大的恐慌苇倡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異雏节,居然都是意外死亡胜嗓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門钩乍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辞州,“玉大人,你說我怎么就攤上這事寥粹”涔” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵涝涤,是天一觀的道長(zhǎng)媚狰。 經(jīng)常有香客問我,道長(zhǎng)阔拳,這世上最難降的妖魔是什么崭孤? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮糊肠,結(jié)果婚禮上辨宠,老公的妹妹穿的比我還像新娘。我一直安慰自己货裹,他們只是感情好嗤形,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弧圆,像睡著了一般赋兵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搔预,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天霹期,我揣著相機(jī)與錄音,去河邊找鬼拯田。 笑死历造,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勿锅。 我是一名探鬼主播帕膜,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼枣氧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼溢十!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起达吞,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤张弛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吞鸭,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寺董,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刻剥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遮咖。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖造虏,靈堂內(nèi)的尸體忽然破棺而出御吞,到底是詐尸還是另有隱情,我是刑警寧澤漓藕,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布陶珠,位于F島的核電站,受9級(jí)特大地震影響享钞,放射性物質(zhì)發(fā)生泄漏揍诽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一栗竖、第九天 我趴在偏房一處隱蔽的房頂上張望暑脆。 院中可真熱鬧,春花似錦划滋、人聲如沸饵筑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽根资。三九已至,卻和暖如春同窘,著一層夾襖步出監(jiān)牢的瞬間玄帕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工想邦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裤纹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓丧没,卻偏偏與公主長(zhǎng)得像鹰椒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呕童,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

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

  • 含義 async函數(shù)是Generator函數(shù)的語法糖漆际,它使得異步操作變得更加方便。 寫成async函數(shù)夺饲,就是下面這...
    oWSQo閱讀 1,993評(píng)論 0 2
  • async 函數(shù) 含義 ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù)奸汇,使得異步操作變得更加方便施符。 async 函數(shù)是...
    huilegezai閱讀 1,259評(píng)論 0 6
  • async 函數(shù) ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便擂找。async 函數(shù)是什么戳吝?一...
    _羊羽_閱讀 2,105評(píng)論 0 1
  • 原文連接:https://blog.csdn.net/sinat_17775997/article/details...
    小豆soybean閱讀 4,256評(píng)論 0 7
  • 本文為阮一峰大神的《ECMAScript 6 入門》的個(gè)人版提純塘雳! babel babel負(fù)責(zé)將JS高級(jí)語法轉(zhuǎn)義欢唾,...
    Devildi已被占用閱讀 1,983評(píng)論 0 4