メインコンテンツへ移動

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();

両者のコード構造は類似し、出力も類似しています(2 つの例として別々に実行):

// 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

その中で 2 行目の 3setTimeout の戻り値(したがって asyncFunction 中では最初のセグメントのみ同期的に実行される)、3 行目の出力は 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 も呼之欲出です(yield から await へ、本質的にはさらに非同期プログラミング体験を向上させただけで、微改進と言えます):

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 行のコード)

今回想一下私たちはどのようにしてこれら 2 つの特性を組み合わせたのか?或者说、これら 2 つの特性のどのメカニズムに依存して、盗版が簡単に実現できたのか?

まず、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()、これで非同期操作を 1 つやり終えてから下のことを続ける待機を実現しました。即ち 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 を取り、すべて紙帯に戳て、コンピュータに塞ぎ、実行結果を取り出す)おや、これ……尼瑪、どうしてエラーが出たの?

コメント

コメントはまだありません

コメントを書く