ホームページ  >  記事  >  ウェブフロントエンド  >  jsによるプロセス制御: Callbacks&Promises&Async/Awaiの解析

jsによるプロセス制御: Callbacks&Promises&Async/Awaiの解析

不言
不言オリジナル
2018-08-25 15:40:551291ブラウズ

この記事の内容は、js でのプロセス制御: Callbacks&Promises&Async/Awai に関するもので、必要な方は参考にしていただければ幸いです。

JavaScript はしばしば「非同期」であると主張します。それはどういう意味ですか?それは開発にどのような影響を及ぼしますか?このアプローチは近年どのように変化しましたか?

次のコードを考えてみましょう:

result1 = doSomething1();
result2 = doSomething2(result1);

ほとんどの言語は、すべての行を同期して処理します。最初の行が実行され、結果が返されます。 2 番目の行は、どれだけ時間がかかっても、最初の行が完了した後に実行されます。

シングルスレッド処理

JavaScript は単一の処理スレッドで実行されます。ブラウザーのタブで実行中は、並列スレッドではページの DOM への変更が発生しないため、他のすべてが停止します。別のスレッドが子ノードを追加しようとしている間に、あるスレッドを別の URL にリダイレクトするのは危険です。

これはユーザーにとって明らかです。たとえば、JavaScript はボタンのクリックを検出し、計算を実行し、DOM を更新します。完了すると、ブラウザはキュー内の次の項目を自由に処理できるようになります。

(補足: PHP などの他の言語もシングル スレッドを使用しますが、Apache などのマルチスレッド サーバーで管理できます。同じ PHP ランタイム ページへの 2 つの同時リクエストにより、分離されたインスタンスを実行する 2 つのスレッドが開始される可能性があります。)

非同期にコールバックを使用する

シングルスレッドでは問題が発生します。 JavaScript が「遅い」プロセス (ブラウザーでの Ajax リクエストやサーバーでのデータベース操作など) を呼び出すとどうなりますか?この操作には数秒、場合によっては数分かかる場合があります。応答を待っている間、ブラウザはロックされます。サーバー上では、Node.js アプリケーションはユーザーのリクエストをそれ以上処理できなくなります。

解決策は非同期処理です。完了を待つ代わりに、結果の準備ができたら別の関数を呼び出すようにプロセスに指示します。これはコールバックと呼ばれ、パラメータとして非同期関数に渡されます。例:

doSomethingAsync(callback1);
console.log('finished');

// call when doSomethingAsync completes
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}

doSomethingAsync() は、コールバック関数を引数として受け入れます (関数への参照のみが渡されるため、オーバーヘッドはほとんどありません)。 doSomethingAsync() にどれだけ時間がかかるかは関係ありません。わかっているのは、callback1() が将来のある時点で実行されるということだけです。コンソールには次のように表示されます:

finished
doSomethingAsync complete

コールバック地獄

通常、コールバックは非同期関数によってのみ呼び出すことができます。したがって、簡潔な匿名インライン関数を使用できます。

doSomethingAsync(error => {
  if (!error) console.log('doSomethingAsync complete');
});

コールバック関数をネストすることにより、2 つ以上の非同期呼び出しを連続して完了できます。例:

async1((err, res) => {
  if (!err) async2(res, (err, res) => {
    if (!err) async3(res, (err, res) => {
      console.log('async1, async2, async3 complete.');
    });
  });
});

残念ながら、これにより悪名高い概念である Callback Hell が導入されます (http://callbackhell.com/)!コードは読みにくく、エラー処理ロジックが追加されるとさらに悪化します。

クライアント側のコーディングではコールバック地獄が発生することは比較的まれです。 Ajax 呼び出しを行って DOM を更新し、アニメーションが完了するのを待っている場合、2 ~ 3 レベルの深さになる可能性がありますが、通常はまだ管理可能です。

状況はオペレーティングシステムまたはサーバープロセスによって異なります。 Node.js API 呼び出しでは、ファイルのアップロードの受信、複数のデータベース テーブルの更新、ログへの書き込み、応答を送信する前にさらに API 呼び出しを行うことができます。

Promises

ES2015 (ES6) では Promises が導入されました。コールバックは引き続き使用できますが、Promise は非同期コマンドを連鎖するためのよりクリーンな構文を提供するため、コマンドをシリアルに実行できます (詳細はこちら)。

Promise ベースの実行を有効にするには、Promise オブジェクトをすぐに返すように非同期コールバック ベースの関数を変更する必要があります。この Promise オブジェクトは、将来のある時点で 2 つの関数 (引数として渡される) のいずれかを実行します:

  • resolve : ハンドラーが正常に完了したときに実行されるコールバック関数

  • reject : ハンドラーが正常に完了したときに実行されるオプションのコールバック関数故障が発生します。

以下の例では、データベース API はコールバック関数を受け入れる connect() メソッドを提供します。外部の asyncDBconnect() 関数は、すぐに新しい Promise を返し、接続が確立されるか失敗した後に、resolve() または拒否() を実行します。

const db = require('database');

// connect to database
function asyncDBconnect(param) {

  return new Promise((resolve, reject) => {

    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });

  });

}

Node.js 8.0 以降では、コールバックベースの関数を実行する util.promisify() ユーティリティが提供されます。 Promise ベースの代替案に変換されます。いくつかの条件があります:

  1. コールバックを最後の引数として非同期関数に渡します

  2. コールバック関数はエラーを指し、その後に値引数が続く必要があります。

例:

// Node.js: promisify fs.readFile
const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);

readFileAsync('file.txt');

さまざまなクライアント ライブラリでも Promise オプションが提供されていますが、独自のオプションをいくつか作成できます:

// promisify a callback function passed as the last parameter
// the callback function must accept (err, data) parameters
function promisify(fn) {
  return function() {
      return new Promise(
        (resolve, reject) => fn(
          ...Array.from(arguments),
        (err, data) => err ? reject(err) : resolve(data)
      )
    );
  }
}

// example
function wait(time, callback) {
  setTimeout(() => { callback(null, 'done'); }, time);
}

const asyncWait = promisify(wait);

ayscWait(1000);

非同期チェーン

Promise を返すものはすべて、.then() での定義に従って開始できます。メソッド 一連の非同期関数呼び出し。それぞれに前のソリューションの結果が渡されます:

asyncDBconnect('http://localhost:1234')
  .then(asyncGetSession)      // passed result of asyncDBconnect
  .then(asyncGetUser)         // passed result of asyncGetSession
  .then(asyncLogAccess)       // passed result of asyncGetUser
  .then(result => {           // non-asynchronous function
    console.log('complete');  //   (passed result of asyncLogAccess)
    return result;            //   (result passed to next .then())
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

同期関数は .then() ブロックでも実行できます。戻り値は、次の .then() に渡されます (存在する場合)。

.catch() メソッドは、以前の拒否がトリガーされたときに呼び出される関数を定義します。この時点で、.then() メソッドは実行されなくなります。チェーン全体で複数の .catch() メソッドを使用して、さまざまなエラーをキャッチできます。

ES2018 では、結果に関係なく最終ロジック (クリーンアップ、データベース接続のクローズなど) を実行する .finally() メソッドが導入されました。現在、Chrome と Firefox でのみサポートされていますが、技術委員会 39 は .finally() ポリフィルをリリースしました。

function doSomething() {
  doSomething1()
  .then(doSomething2)
  .then(doSomething3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    // tidy-up here!
  });
}

使用Promise.all()进行多个异步调用

Promise .then()方法一个接一个地运行异步函数。如果顺序无关紧要 - 例如,初始化不相关的组件 - 同时启动所有异步函数并在最后(最慢)函数运行解析时结束更快。

这可以通过Promise.all()来实现。它接受一组函数并返回另一个Promise。例如:

Promise.all([ async1, async2, async3 ])
  .then(values => {           // array of resolved values
    console.log(values);      // (in same order as function array)
    return values;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

如果任何一个异步函数调用失败,则Promise.all()立即终止。

使用Promise.race的多个异步调用()

Promise.race()与Promise.all()类似,只是它会在first Promise解析或拒绝后立即解析或拒绝。只有最快的基于Promise的异步函数才能完成:

Promise.race([ async1, async2, async3 ])
  .then(value => {            // single value
    console.log(value);
    return value;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });

但是有什么别的问题吗?

Promises 减少了回调地狱但引入了别的问题。

教程经常没有提到_整个Promise链是异步的。使用一系列promise的任何函数都应返回自己的Promise或在最终的.then(),. catch()或.finally()方法中运行回调函数。

学习基础知识至关重要。

Async/Await

Promises 可能令人生畏,因此ES2017引入了async and await。 虽然它可能只是语法糖,它使Promise更完善,你可以完全避免.then()链。 考虑下面的基于Promise的示例:

function connect() {

  return new Promise((resolve, reject) => {

    asyncDBconnect('http://localhost:1234')
      .then(asyncGetSession)
      .then(asyncGetUser)
      .then(asyncLogAccess)
      .then(result => resolve(result))
      .catch(err => reject(err))

  });
}

// run connect (self-executing function)
(() => {
  connect();
    .then(result => console.log(result))
    .catch(err => console.log(err))
})();

用这个重写一下async/await:

  • 外部函数必须以async语句开头

  • 对异步的基于Promise的函数的调用必须在await之前,以确保在下一个命令执行之前完成处理。

async function connect() {

  try {
    const
      connection = await asyncDBconnect('http://localhost:1234'),
      session = await asyncGetSession(connection),
      user = await asyncGetUser(session),
      log = await asyncLogAccess(user);

    return log;
  }
  catch (e) {
    console.log('error', err);
    return null;
  }

}

// run connect (self-executing async function)
(async () => { await connect(); })();

await有效地使每个调用看起来好像是同步的,而不是阻止JavaScript的单个处理线程。 此外,异步函数总是返回一个Promise,因此它们可以被其他异步函数调用。

async/await 代码可能不会更短,但有相当大的好处:

1、语法更清晰。括号更少,错误更少。

2、调试更容易。可以在任何await语句上设置断点。
3、错误处理更好。try / catch块可以与同步代码一样使用。

4、支持很好。它在所有浏览器(IE和Opera Mini除外)和Node 7.6+中都得到了支持。

但是并非所有都是完美的......

切勿滥用async/await

async / await仍然依赖于Promises,它最终依赖于回调。你需要了解Promises是如何工作的,并且没有Promise.all()和Promise.race()的直接等价物。并且不要忘记Promise.all(),它比使用一系列不相关的await命令更有效。

同步循环中的异步等待

在某些时候,您将尝试调用异步函数中的同步循环。例如:

async function process(array) {
  for (let i of array) {
    await doSomething(i);
  }
}

它不会起作用。这也不会:

async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}

循环本身保持同步,并且总是在它们的内部异步操作之前完成。

ES2018引入了异步迭代器,它与常规迭代器一样,但next()方法返回Promise。因此,await关键字可以与for循环一起用于串行运行异步操作。例如:

async function process(array) {
  for await (let i of array) {
    doSomething(i);
  }
}

但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:

const
  todo = ['a', 'b', 'c'],
  alltodo = todo.map(async (v, i) => {
    console.log('iteration', i);
    await processSomething(v);
});

await Promise.all(alltodo);

这具有并行运行任务的好处,但是不可能将一次迭代的结果传递给另一次迭代,并且映射大型数组可能在性能消耗上是很昂贵。

try/catch 有哪些问题了?

如果省略任何await失败的try / catch,async函数将以静默方式退出。如果您有一组很长的异步await命令,则可能需要多个try / catch块。

一种替代方案是高阶函数,它捕获错误,因此try / catch块变得不必要(thanks to @wesbos for the suggestion):

async function connect() {

  const
    connection = await asyncDBconnect('http://localhost:1234'),
    session = await asyncGetSession(connection),
    user = await asyncGetUser(session),
    log = await asyncLogAccess(user);

  return true;
}

// higher-order function to catch errors
function catchErrors(fn) {
  return function (...args) {
    return fn(...args).catch(err => {
      console.log('ERROR', err);
    });
  }
}

(async () => {
  await catchErrors(connect)();
})();

但是,在应用程序必须以与其他错误不同的方式对某些错误做出反应的情况下,此选项可能不实用。

尽管有一些陷阱,async / await是JavaScript的一个优雅补充。

JavaScript 旅程

异步编程是一项在JavaScript中无法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入深层嵌套的函数中。

Promises 抽象回调,但有许多语法陷阱。 转换现有函数可能是一件苦差事,而.then()链仍然看起来很混乱。

幸运的是,async / await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!

相关推荐:

整理Javascript流程控制语句学习笔记_javascript技巧

JavaScript中使用Callback控制流程介绍_javascript技巧

以上がjsによるプロセス制御: Callbacks&Promises&Async/Awaiの解析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。