跳到主要內容
黯羽輕揚每天積累一點點

從 Generator 到 Async function

免費2018-11-03#JS#JavaScript async#js async function#async function polyfill#JS异步函数#JS异步函数是语法糖

18 行代碼實現 Async function 特性

寫在前面

說到異步函數,不由地想起 [Wind.js](/articles/向 windjs 致敬-node 異步流程控制 4/),以及老趙的遠見:

Wind.js 在 JavaScript 異步編程領域絕對是一個創新,可謂前無來者。有朋友就評價說「在看到 Wind.js 之前,真以為這是不可能實現的」,因為 Wind.js 事實上是用類庫的形式「修補」了 JavaScript 語言,也正是這個原因,才能讓 JavaScript 異步編程體驗獲得質的飛躍。 ——2012 年 7 月

ES2017 的 async&await 從 promise,generator 一路輾轉走來,而 Wind 早在 6 年前就看到了這一天,並提前實現了願景

一。yield 與 await

為什麼說 Async function 是從 [Promise](/articles/完全理解 promise/),[Generator](/articles/generator(生成器)-es6 筆記 2/) 一路走來的?

因為異步函數與 Generator 特性有著千絲萬縷的關係,比如,語義上都有暫停的意思:

  • yield:讓步,歇會兒喘口氣

  • await:橋多麻袋

先對比一個最簡單的場景:

// generator
function* gen() {
  console.log('Do step 1');
  yield 'Until step1 completed';
  console.log('Do step 2');
}
let iter = gen();
iter.next();
iter.next();

// async function
async function f() {
  console.log('Do step 1');
  await 'Until step1 completed';
  console.log('Do step 2');
}
f();

二者代碼結構相似,並且輸出也類似(作為兩個例子分開執行):

// generator
Do step 1
Do step 2
{value: undefined, done: true}

// async function
Do step 1
Do step 2
Promise?{<resolved>: undefined}

二。暫停呢?

生成器能讓執行流「喘口氣」,能讓停不下來的東西暫停,能用來重構循環,能駕馭無限序列,能包裝迭代器。。。好處多多

(摘自 [generator(生成器)_ES6 筆記 2](/articles/generator(生成器)-es6 筆記 2/#articleHeader9))

但上例中好像並沒有看到暫停的效果,我們加點 log,讓一切更明顯一些:

// generator
function* gen() {
  console.log('Do step 1');
  yield 'Until step1 completed';
  console.log('Do step 2');
}
let iter = gen();
iter.next();
console.log('generator 抽根兒煙');
iter.next();

// async function
async function f() {
  console.log('Do step 1');
  await 'Until step1 completed';
  console.log('Do step 2');
}
f();
console.log('async function 抽根兒煙');

這次不關注各自的返回值(上面已經看過了),連在一起執行,輸出結果如下:

Do step 1
generator 抽根兒煙
Do step 2
Do step 1
async function 抽根兒煙
Do step 2

輸出沒什麼差異,但 log('xxx 抽根兒煙') 所在的位置有差異

實際區別在於,上例中 Generator 的執行過程是純同步的,而 async function 的執行過程含有異步的部分,用 Generator 來描述的話,相當於:

// generator 假裝 async function
function* gen() {
  console.log('Do step 1');
  yield Promise.resolve('Until step1 completed');
  console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假裝 async function 抽根兒煙');

// 輸出結果
Do step 1
generator 假裝 async function 抽根兒煙
Do step 2

三。近一點,更近一點

更進一步地,很容易用 Generator 去實現 Async function 特性

function asyncFunction(gen, ...args) {
  return new Promise((resolve, reject) => {
    resolve(safeNext(gen(...args)));
  });
}

function safeNext(iter, last) {
  let step;
  try {
    step = iter.next(last);
  } catch(ex) {
    step = iter.throw(ex);
  }

  return Promise.resolve(step.value)
    .catch(ex => iter.throw(ex).value)
    .then(result => step.done ? result : safeNext(iter, result))
}

P.S.Github repo 地址 ayqy/asyncFunction

試玩一下:

asyncFunction(function* (){
  console.log('Do step 1');
  // Wait 100ms
  let x = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  console.log(`x = ${x}`);
  x = yield x * 2;
  console.log(`All steps passed, got ${x}`);
  return x;
}).then(result => {
  console.log(`Final result ${result}`);
});
let intervalId = setInterval(console.log.bind(console, 'tick'), 10);
setTimeout(() => {
  clearInterval(intervalId);
}, 100);

會得到類似輸出:

Do step 1
3
⑨tick
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
tick

其中第二行的 3setTimeout 返回值(因此 asyncFunction 中只有第一段是同步執行的),第三行輸出 9 次 'tick' 表示過了 90 多 ms,此時 Wait 100ms 結束了,接著執行剩餘部分直到結束

另外,還有一個難以察覺的細節是,本例中剩餘部分的執行不會被 interval 回調打斷(即便間隔極短),例如:

asyncFunction(function* (){
  setTimeout(console.log.bind(console, '#0'), 0)
  console.log('Do step 1');
  // Wait 100ms
  let x = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  setTimeout(console.log.bind(console, '#1'), 0)
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  setTimeout(console.log.bind(console, '#2'), 0)
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  setTimeout(console.log.bind(console, '#3'), 0)
  console.log(`x = ${x}`);
  x = yield x * 2;
  setTimeout(console.log.bind(console, '#4'), 0)
  console.log(`All steps passed, got ${x}`);
  return x;
}).then(result => {
  console.log(`Final result ${result}`);
});

輸出結果是:

Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

#1, 2, 3, 4 最後輸出,這與任務類型有關,具體見 macrotask 與 microtask

對比正版 async function:

(async function(){
  setTimeout(console.log.bind(console, '#0'), 0)
  console.log('Do step 1');
  // Wait 100ms
  let x = await new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  setTimeout(console.log.bind(console, '#1'), 0)
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  setTimeout(console.log.bind(console, '#2'), 0)
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  setTimeout(console.log.bind(console, '#3'), 0)
  console.log(`x = ${x}`);
  x = await x * 2;
  setTimeout(console.log.bind(console, '#4'), 0)
  console.log(`All steps passed, got ${x}`);
  return x;
})().then(result => {
  console.log(`Final result ${result}`);
});

輸出完全一致:

Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

四。語法糖?

基本語法形式如下:

async function name([param[, param[, ... param]]]) {
  statements
}

需要知道 2 點:

  • await 關鍵字只能出現在 Async function 裡,否則報錯

  • Async function 的返回值是 Promise

實際上,async function 共有 4 種形式:

  • 函數聲明:async function foo() {}

  • 函數表達式:const foo = async function () {};

  • 方法定義:let obj = { async foo() {} }

  • 箭頭函數:const foo = async () => {};

例如:

async function fetchJson(url) {
  try {
    console.log('Starting fetch');
    let request = await fetch(url);
    let text = await request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}

// test
fetchJson('https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));
console.log('Fetching...');

輸出:

Starting fetch
Fetching...
undefined
{name: "emoutils",?…}

咦,異步函數貌似並不「異步」,Async function 函數體的第一段(第一個 await 之前的部分)是同步執行的,類似於:

new Promise(resolve => {
  console.log('Starting fetch');
  setTimeout(resolve.bind(null, 'data'), 100);
}).then(data => console.log(data));
console.log('Fetching...');

同樣,很容易把這個東西換成我們的盜版:

asyncFunction(function* fetchJson(url) {
  try {
    console.log('Starting fetch');
    let request = yield fetch(url);
    let text = yield request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}, 'https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));

// test
console.log('Fetching...');

事實上我們做了 3 件事:

  • 把函數體用 Generator 包起來,await 都換成 yield

  • 去掉 asyncfunction 之間的空格並駝峰命名

  • 把參數挪到 Generator 後面去

如果把這 3 件事通過編譯轉換屏蔽掉的話(甚至簡單匹配替換就能做到):

function afunction(templateData) {
  const source = templateData;
  // ...一頓操作把上面字符串內容轉換成
  let params = ['url'];
  let transformed = `function* fetchJson(url) {
    try {
      console.log('Starting fetch');
      let request = yield fetch(url);
      let text = yield request.text();
      return JSON.parse(text);
    } catch(error) {
      console.error(error);
    }
  }`;

  return function(...args) {
    return asyncFunction(new Function(...params, `return ${transformed}`)(), ...args);
  };
}

async function 特性就被盜版方案完全取代了,語法形式也可以變得更相近:

afunction`(url) => {
  try {
    console.log('Starting fetch');
    let request = await fetch(url);
    let text = await request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}`('https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));

P.S. 這裡應用了 ES2015 標籤模板(tagged templates)特性,具體見 [模板字符串_ES6 筆記 3](/articles/模板字符串-es6 筆記 3/#articleHeader6)

那麼,Async function 是語法糖嗎?

可以認為是。因為有了 Generator 特性後,Async function 也就呼之欲出了(從 yieldawait,本質上只是進一步提升了異步編程體驗,算是微改進):

Internally, async functions work much like generators, but they are not translated to generator functions.

但語言層面的特性支持要比類似編譯轉換的替代方案更具優勢,體現在性能、錯誤追蹤(乾淨的調用棧)、與其它特性無縫貼合(如箭頭函數、方法定義)等方面

異步編程體驗

從編程體驗上來看,Async function 特���帶來的提升在於:

  • 以同步形式編寫異步代碼,異步、回調等概念被淡化了

  • try-catch 能夠捕獲到異步操作中的異常

能讓含有異步操作的代碼塊仍然順序執行,這無疑是最好的異步編程體驗了:

// callback reqXXX(參數,成功回調,失敗回調)
reqLogin(password, reqOrderNo, notFound);
  reqOrderNo(uid, reqOrderDetail, notFound);
    reqOrderDetail(orderNo, render, boom);
      render(data);

// promise
promisifiedReqLogin(password)
  .then(({ uid }) => promisifiedReqOrderNo(uid), notFound)
  .then(({ orderNo }) => promisifiedReqOrderDetail(orderNo), notFound)
  .then(({ data }) => render(data))
  .catch(boom)

// async function
async function renderPage(password) {
  let uid, orderNo;
  try {
    uid = await promisifiedReqLogin(password);
    orderNo = await promisifiedReqOrderNo(uid);
  } catch(ex) {
    notFound(ex);
  }

  let data = await promisifiedReqOrderDetail(orderNo);
  return render(data);
}
renderPage().catch(boom);

data = await fetchData(),僅此而已。回調的概念不復存在,減輕了大腦跟著異步操作入棧出棧的負擔,畢竟

代碼是寫給人看的,附帶可以在機器上運行

(摘自 [寫好 JavaScript](/articles/寫好 javascript/))

五。淵源

至此,我們已經用 Generator 和 Promise 特性實現了盜版 Async function,甚至沒費多少工夫(僅 18 行代碼)

現在回想一下我們是如何把這兩個特性組合起來的?或者說,依靠這兩個特性的哪些機制,讓盜版得以輕鬆實現?

首先,要實現 Async function 的話,最关键的特性是 Generator,通過 yield 讓順序執行流停下來,才有「等待」一說:

function* infSeq() {
  let i = 0;
  // 不會發生死循環喲,yield 讓 while true「停」下來了
  while (true) {
    console.log(i);
    yield i++;
  }
}

// test
let iter = infSeq();
// 輸出 0, 1, 2...
iter.next();
iter.next();
iter.next();

能「等待」了,那麼等誰呢?直接等異步操作嗎?如何區分異步操作?

沒錯,該 Promise 上場了:

// generator 假裝 async function
function* gen() {
  console.log('Do step 1');
  yield Promise.resolve('Until step1 completed');
  console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假裝 async function 抽根兒煙');

只要 next().value 是 Promise,那就肯定是異步操作,等它完成了再 next(),這樣就實現了等待一個異步操作做完再繼續下面的事情,即 Async function 特性

上層概念上來看,三者關係如下:

Async function = 調度機(Generator) + 異步任務(Promise)

其中,Generator 這個調度機的作用在於:

  • 分片(拆不開怎麼等):將函數體順序代碼塊拆分成幾段

  • 調度(拆開了怎麼執行):從外部控制這些片段的執行流,如 next()throw()

Promise 作為異步任務模型,主要特點如下:

  • 狀態丟棄:一次性的 Promise 對象,用完即扔(then() 等都返回新 Promise)

  • 任務組合:可以通過類似 resolve(promise) 的方式形成任務鏈,結合 all()race() 等控制其順序

  • 錯誤上拋:類似於冒泡的異常處理機制,沿任務鏈向上拋出異常,簡化了異步任務的異常捕獲

Generator 並不直接調度 Promise(調度的對象是被拆開的片段),但它關注每一段的執行結果,如果結果是 pending Promise,就等到不 pending 了,再控制下一段執行

所以,Promise 只是配角兒,可以替換成任意的異步任務模型,其主要作用在於告知 Generator 這裡有個異步操作得等一下:

調度機:(把一段代碼戳在紙帶上,塞進計算機,取出執行結果)咦,這是個啥?
異步任務:Hey,我是個異步任務啊,還沒完事兒,完了我告訴你
調度機:好,我抽根兒煙(頭像變灰)
異步任務:完事了完事了,結果是 xxx
調度機:(立即上線,拿起下一段代碼和 xxx,都戳在紙帶上,塞進計算機,取出執行結果)咦,這……尼瑪,咋還出錯了捏?

評論

暫無評論,快來發表你的看法吧

提交評論