外国語の「非同期 JavaScript の進化」では、JavaScript 非同期関数の開発の歴史を整理しました。まず、非同期関数はコールバック関数を通じて実現され、その後、Promise/A およびジェネレーター関数を経て実現されることになります。非同期関数であること。この記事を翻訳してくれた Jingzhuang に感謝します。内容は次のとおりです:
ここで、長年にわたる JavaScript 非同期関数の開発を振り返ってみましょう。
コールバック関数 Callbacks
コールバック関数からすべてを開始する必要があるようです。
非同期 JavaScript
ご存知のとおり、JavaScript では、非同期プログラミングは JavaScript 言語の第一級市民関数を通じてのみ実行できます。この方法は、ある関数を別の関数のパラメーターとして渡すことができることを意味します。関数内で呼び出されます (つまり、コールバック関数)。これが、コールバック関数が生まれた理由です。関数をパラメータとして別の関数 (この時点では高階関数と呼ばれます) に渡すと、関数内でこの関数を呼び出して、対応するタスクを完了できます。コールバック関数には戻り値がなく (リターンを使用しないでください)、関数内で特定のアクションを実行するためにのみ使用されます。 例を見てみましょう:
Something.save(function(err) { if (err) { //error handling return; // 没有返回值 } console.log('success'); });
上記の例では、Node.js のすべてのコア モジュールと NPM リポジトリのほとんどのモジュールの特徴の 1 つでもあるエラー優先コールバック関数 (error-first callbacks) を示しました。書き込み時にはこの機能に従います。
コールバック関数の多用に関する課題:
コードを合理的に整理できないと、コールバック地獄 (コールバック地獄) が発生しやすくなり、コードが他の人に理解されにくくなります。
エラー処理コードは見落とされがちです。
return ステートメントを使用して値を返すことはできません。また、throw キーワードは使用できません。
これらの理由から、JavaScript の世界では、非同期 JavaScript 開発を容易にする実現可能なソリューションを常に探しています。
考えられる解決策の 1 つは、async モジュールです。コールバック関数を長い間扱ってきた人であれば、JavaScript で何かを並列または直列に実行したり、非同期関数を使用して配列内の何かをマップしたりする場合、どれほど複雑であるかを深く理解しているかもしれません。は非同期関数を使用する要素です。したがって、これらの問題を解決する非同期モジュールを作成してくれた Caolan McMahon に感謝します。
async モジュールを使用すると、次の方法でコードを簡単に作成できます。
async.map([1, 2, 3], AsyncSquaringLibrary.square, function(err, result){ // result will be [1, 4, 9] });
async モジュールはある程度の利便性をもたらしますが、まだ単純ではなく、コードが読みにくいため、Promise が登場しました。
約束
現在の JavaScript 非同期標準は 2012 年に遡り、ES6 まで利用可能になりませんでした。ただし、Promise という用語は JavaScript コミュニティによって発明されたものではありません。この用語は、1976 年にダニエル P. フリードマンによって発表された記事に由来しています。
Promise は、非同期操作の最終結果を表します。
ここで、Promise を使用して、上記のコードで完了したタスクを完了します。Promise スタイルのコードは次のとおりです。
Something.save() .then(function() { console.log('success'); }) .catch(function() { //error handling })
Promise でもコールバック関数が使用されていることがわかります。コールバック関数は then メソッドと catch メソッドの両方に渡され、Promise が満たされた場合と拒否された場合にそれぞれ実行されます。 Promise 関数のもう 1 つの利点は、関数を連鎖させて一連のタスクを完了できることです。たとえば、次のようなコードを書くことができます:
saveSomething() .then(updateOtherthing) .then(deleteStuff) .then(logResults);
既製の Promise がない場合は、いくつかの Promise ライブラリを使用する必要がある場合があります。一般的な選択肢は、bluebird を使用することです。 これらのライブラリは、ネイティブ ソリューションよりも多くの機能を提供する可能性があり、Promise/A 標準で指定された機能に限定されません。
でも、なぜ砂糖を使った方法を使わないのでしょうか?最初に「Promise: 拡張機能の問題」という記事を読むことをお勧めします。 Promise の詳細については、Promise/A 標準を参照してください。
ほとんどのライブラリがコールバック インターフェイスのみを公開する場合、どのように Promise を使用すればよいのでしょうか?
これは非常に簡単です。現時点で行う必要があるのは、Promise を使用してコールバックを含む関数呼び出し本体をラップすることだけです。例:
コールバック スタイルのコードは次のようになります:
function saveToTheDb(value) { db.values.insert(value, function (err, user) { if (err) throw err; // todo: insert user to db }); }
ここで、Promise スタイルの呼び出しをサポートするコードに変更します。
function saveToTheDb(value) { return new Promise(function(resolve, reject) { db.values.insert(value, function(err, user) { // remember error first ;) if (err) { return reject(err); // don't forget to return here } resolve(user); }) } }
かなりの数のライブラリまたはフレームワークがすでに両方のメソッドをサポートしています。つまり、コールバック スタイルと Promise スタイルの API インターフェイスの両方を提供しています。したがって、ライブラリを外部にも提供したい場合、ベスト プラクティスは両方のインターフェイスを同時に提供することです。この目的を達成するには、次のメソッドを簡単に使用できます:
function foo(cb) { if (cb) { return cb(); } return new Promise(function (resolve, reject) { }); }
或者更简单些,你可以从只提供Promise风格的接口开始后,并使用诸如 callbackify这样的工具来达到向后兼容的目的。其实Callbackify所做的工作和上面的代码片段类似,但在实现上使用了一个更通用的方法, 我建议你可以去阅读Callbackify的源代码。
生成器Generators/ yield
JavaScript 生成器是个相对较新的概念, 它是ES6(也被称为ES2015)的新特性。想象下面这样的一个场景:
当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。
上面描述的场景正是JavaScript生成器函数所致力于解决的问题。当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。
function* foo () { var index = 0; while (index < 2) { yield index++; //暂停函数执行,并执行yield后的操作 } } var bar = foo(); // 返回的其实是一个迭代器 console.log(bar.next()); // { value: 0, done: false } console.log(bar.next()); // { value: 1, done: false } console.log(bar.next()); // { value: undefined, done: true }
更进一步的,如果你想更轻松的使用生成器函数来编写异步JavaScript代码,我们可以使用 co 这个库,co是著名的tj大神写的。
Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
使用co,前面的示例代码,我们可以使用下面的代码来改写:
co(function* (){ yield Something.save(); }).then(function() { // success }) .catch(function(err) { //error handling });
你可能会问:如何实现并行操作呢?答案可能比你想象的简单,如下(其实它就是Promise.all而已):
yield [Something.save(), Otherthing.save()]; Async/ await
在ES7(还未正式标准化)中引入了Async函数的概念,目前如果你想要使用的话,只能借助于babel 这样的语法转换器将其转为ES5代码。(提醒一点:我们现在讨论的是async关键字,而不是NPM中的async包)。
简而言之,使用async关键字,你可以轻松地达成之前使用生成器和co函数所做到的工作。当然,除了hack之外。
也许你会问,是否在ES7中有了async关键字,yield就变得不是那么重要了?
实际上,使用yield实现异步也不过是一种hack罢了,yield意味着懒次序(lazy sequences)和迭代器。 而await能够完美的分离这两点,首先让yield用于其最初的目的,其次使用await来执行异步操作。
在这背后,async函数实际使用的是Promise,也就是为什么async函数会返回一个Promise的原因。
因此,我们使用async函数来完成类似于前面代码所完成的工作,可以使用下面这样的方式来重新编写代码:
async function save(Something) { try { await Something.save(); // 等待await后面的代码执行完,类似于yield } catch (ex) { //error handling } console.log('success'); }
正如你看到的那样,使用async函数,你需要在函数声明的最前面加上async关键字。这之后,你可以在函数内部使用await关键字了,作用和之前的yield作用是类似的。
使用async函数完成并行任务与yiled的方式非常的相似,唯一不同的是,此时Promise.all不再是隐式的,你需要显示的调用它:
async function save(Something) { await Promise.all[Something.save(), Otherthing.save()] }
Koa也支持async函数,如果你也在使用koa,那么你现在就可以借助babel使用这一特性了。
import koa from koa; let app = koa(); app.experimental = true; app.use(async function (){ this.body = await Promise.resolve('Hello Reader!') }) app.listen(3000);
以上内容给大家分享了JavaScript异步函数发展历程,希望对大家有所帮助。