Javaでpromiseを実装します

すべての良い一日。 今日は、JSエンジンのpromiseメカニズムの実装をどのように書いたかについてお話したいと思います。 ご存知のように、新しいECMA Script 6標準はそれほど前にリリースされておらず、promiseの概念は非常に興味深く、またWeb開発者によって使用される多くの場所に見えます。 したがって、現代のJSエンジンでは、これは確かに必要なものです。

注意:この記事には多くのコードがあります。 プロジェクト全体が1人で作成され、まだベータ版であるため、コードは美しく高品質であるように見せかけていません。 この物語の目的は、すべてが内部でどのように機能するかを示すことです。 さらに、少し適合させた後、このコードを使用して、JavaScriptに関係なく、純粋にJavaでプロジェクトを作成できます。



コードを書き始める最初のことは、すべて最終的にどのように機能するかを学ぶことです。 結果のモジュールのアーキテクチャは、プロセス中に大きく決定されました。



Promiseとは何ですか?



Promiseは特別なオブジェクトであり、作成されると保留状態になります(0に等しい定数にします)。



次に、オブジェクトは、作成時にコンストラクターに渡された関数の実行を開始します。 関数が渡されなかった場合-ES6標準に従って、 引数をスローする必要があるのは関数の例外ではありません 。 ただし、Java実装では、何も投げることができず、オブジェクトを「現状のまま」作成することができます(その後、追加のロジックを追加します。これについては後で説明します)。



したがって、コンストラクターは関数を受け入れます。 エンジンでは、これはcallメソッドを実装するFunctionクラスのオブジェクトです。 このメソッドを使用すると、実行コンテキスト、引数を持つベクトル、および呼び出しモード(コンストラクターまたは通常モードとしての呼び出し)を定義するブールパラメーターを使用して、関数を呼び出すことができます。



さらに、この関数はオブジェクトのフィールドに書き込まれ、呼び出すことができます。



public static int PENDING = 0; public static int FULFILLED = 1; public static int REJECTED = 2; ... private int state = 0; private Function func;
      
      





同時に、ここでは、残りの2つの状態の定数と、オブジェクトの現在の状態を格納するintフィールドを作成します。



そのため、標準に従って、実行中の関数は2つの関数のいずれかを呼び出すことができます(最初の2つの引数として渡されるため、適切な方法で関数シグネチャで名前を指定する必要があります)。 通常、簡単にするために解決や拒否などの方法を使用します。



これらはJavaScriptの観点からは通常の関数です。つまり、Functionオブジェクトはエンジンの観点からのものです。 それらにもフィールドを追加します。



  public Function onFulfilled = null; public Function onRejected = null;
      
      





これらの関数は、メインの作業関数によっていつでも呼び出すことができます。つまり、そのスコープ内になければなりません。 さらに、作業を終えると、オブジェクトの状態をそれぞれ実現および拒否に変更する必要があります。 私たちの関数はプロミスについて何も知りません(知ってはいけません)。 したがって、それらについて認識し、状態の変更を開始できる一種のラッパーを作成する必要があります。



また、オブジェクトにsetState()メソッドが必要です(追加のチェックが必要です。たとえば、状態が既に満たされている拒否されている場合、状態を変更する権利はありません)。



オブジェクトのコンストラクタを扱いましょう:



  public Promise(Function f) { func = f; onFulfilled = new PromiseHandleWrapper(this, null, Promise.FULFILLED); onRejected = new PromiseHandleWrapper(this, null, Promise.REJECTED); if (f != null) { Vector<JSValue> args = new Vector<JSValue>(); args.add(onFulfilled); args.add(onRejected); func.call(null, args, false); } }
      
      





ここではすべてが明らかなようです。 関数が渡された場合、すぐに呼び出す必要があります。 そうでない場合は、まだ何もしていません(そして、オブジェクトは保留状態を保持しています)。



次に、これらのハンドラー自体をインストールすることについて説明します(結局、メイン関数では、名前を正式なパラメーターとして宣言するだけです)。 この標準には、Promise.then(解決、拒否)、Promise.then(解決)(Promise.then(解決、null)と同等)、Promise.catch(拒否)(Promise.then(null、reject)と同等)の3つのオプションが用意されています))。



then関数に関しては、2つの引数を詳細に使用してメソッドを実装し、残りの2つを「ショートカット」として作成することが最善であることは明らかです。 だから私たちはやる:



  public Promise then(Function f1, Function f2) { if (state == Promise.FULFILLED || state == Promise.REJECTED) { onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED); onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED); onFulfilled.call(null, new Vector<JSValue>(), false); return this; } ... onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED); onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED); if (func != null) { String name1 = func.getParamsCount() > 0 ? func.getParamName(0) : "resolve"; String name2 = func.getParamsCount() > 1 ? func.getParamName(1) : "reject"; func.injectVar(name1, onFulfilled); func.injectVar(name2, onRejected); } if (f1 != null) has_handler = true; if (f2 != null) has_error_handler = true; return this; }
      
      





最後に、リンクを自分自身に返します。これは、将来の有望なチェーンの実装に必要です。



メソッドの最初にどのブロックがありますか? ただし、実際にハンドラを実行できるのは、それから最初に呼び出した前でもあります(これは起こりますが、これは完全に正常です)。 この場合、すぐにメソッドに渡されたハンドラーから目的のハンドラーを呼び出す必要があります。



省略記号の代わりに、もう少し後に別のコードがあります。



次は、必須フィールドへのハンドラーのインストールです。



そして、ここが最も興味深い部分です。 作業関数の実行に時間がかかると仮定します(ネットワーク経由のリクエスト、またはケーススタディの場合は単にsetTimeout)。 この場合、基本的には実行されますが、後でコードを実行する多数のオブジェクト(タイマー、ネットワークXmlHttpRequestインターフェイスなど)が作成されます。 そして、これらのオブジェクトは関数のスコープにアクセスできます!



したがって、必要な変数をスコープに追加するのにまだ手遅れではないかもしれません(手遅れの場合、コードはメソッドの先頭で実行されます)。 これを行うには、Functionクラスで新しいメソッドを作成します。



  public void injectVar(String name, JSValue value) { body.scope.put(name, value); } public void removeVar(String name) { body.scope.remove(name); }
      
      





実際には、2番目の方法は必要ありません。完全性のために作成されたものです。



次は、ショートカットを実装するときです。



  public Promise then(Function f) { return then(f, null); } public Promise _catch(Function f) { return then(null, f); }
      
      





catchはjavaの予約語であるため、アンダースコアを追加する必要がありました。



次に、setStateメソッドについて説明します。 最初の近似では、次のようになります。



  public void setState(int value) { if (this.state > 0) return; this.state = value; }
      
      





さて、ハンドラーから、より正確には、その上のラッパーから状態を変更できます。 ラッパーをやってみましょう:



 public class PromiseHandleWrapper extends Function { public PromiseHandleWrapper(Promise p, Function func, int type) { this.promise = p; this.func = func; this.to_state = type; } @Override public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) { return call(context, args); } @Override public JSValue call(JSObject context, Vector<JSValue> args) { JSValue result; if (func != null) { Block b = getCaller(); if (b == null) { b = func.getParentBlock(); while (b.parent_block != null) { b = b.parent_block; } } func.setCaller(b); result = func.call(context, args, false); } else { result = Undefined.getInstance(); } promise.setResult(result); promise.setState(to_state); return promise.getResult(); } @Override public JSError getError() { return func.getError(); } private Promise promise; private Function func; private int to_state = 0; }
      
      





ラッパーには2つのタイプがありますが、クラスは1つです。 また、整数フィールドto_stateがタイプを担当します。 それは悪くないようです:)



ラッパーには、その機能とその約束の両方へのリンクがあります。 これは非常に重要です。



コンストラクターですべてが明確になったので、関数クラスのメソッドをオーバーライドする呼び出しメソッドを見てみましょう。 JSインタープリターの場合、ラッパーには同じ関数があります。つまり、呼び出したり、値を取得したりできる、同じインターフェースを持つオブジェクトです。



まず、ラッパーが関数に呼び出されたときに受け取ったCallerオブジェクトをスローする必要があります。これは、少なくとも正しい例外がポップアップするために必要です。



次に、関数を呼び出し、その実行結果をフィールドに保存します。 同時に、promisオブジェクトに設定し、そこに別のsetResultメソッドを作成します:



  public JSValue getResult() { return result; } public void setResult(JSValue value) { result = value; }
      
      





最後の行についてはまだ説明しません。これは連鎖に必要です。 最も些細なケースでは、受信および送信した値と同じ値がそこに返されます。



重要な点:作業関数は、thenまたはcatchメソッドを呼び出す前に、resolveまたはrejectを呼び出すことができます(または、まったく呼び出さない場合があります)。 例外を持たないように、プロミスを作成するときに、ハンドラー関数を持たない2つの「デフォルト」ラッパーを作成します。 呼び出されたとき、彼らは私たちの約束の状態を変更するだけです(そして呼び出されたとき、これは考慮されます)。



追いかける約束



要するに、追跡はp.then(f1、f2).then(f3、f4).catch(f5)のようなものを書く能力です。

そのため、thenメソッドと_catchメソッドはPromiseオブジェクトを返します。



標準が最初に伝えることは、thenメソッドは、既存のハンドラーがある場合、新しいプロミスを作成してチェーンに追加する必要があるということです。 約束は互いに等しくなければならないので、線形リストを保存する主要な約束を持たないようにします。各約束は、以下へのリンクのみを保存します(最初はnullです)。



  public Promise then(Function f1, Function f2) { if (state == Promise.FULFILLED || state == Promise.REJECTED) { onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED); onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED); onFulfilled.call(null, new Vector<JSValue>(), false); return this; } if (has_handler || has_error_handler) { if (next != null) { return next.then(f1, f2); } Promise p = new Promise(null); p.then(f1, f2); next = p; return p; } onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED); onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED); if (func != null) { String name1 = func.getParamsCount() > 0 ? func.getParamName(0) : "resolve"; String name2 = func.getParamsCount() > 1 ? func.getParamName(1) : "reject"; func.injectVar(name1, onFulfilled); func.injectVar(name2, onRejected); } if (f1 != null) has_handler = true; if (f1 != null) has_error_handler = true; return this; } ... private Promise next = null;
      
      





不足しているブロックは次のとおりです。すでに次の約束がある場合、呼び出しを彼に転送して終了します(必要に応じて、彼は最後まで転送します)。 そして、存在しない場合は、メソッドで受け取ったハンドラーを作成して割り当て、その後、それを既に返します。 すべてがシンプルです。



次に、setStateメソッドを終了します。



  public void setState(int value) { if (this.state > 0) return; this.state = value; Vector<JSValue> args = new Vector<JSValue>(); if (result != null) args.add(result); if (value == Promise.FULFILLED && next != null) { if (onFulfilled.getError() == null) { if (result != null && result instanceof Promise) { ((Promise)result).then(next.onFulfilled, next.onRejected); next = (Promise)result; } else { result = next.onFulfilled.call(null, args, false); } } else { args = new Vector<JSValue>(); args.add(onFulfilled.getError().getValue()); result = next.onRejected.call(null, args, false); } } if (value == Promise.REJECTED && !has_error_handler && next != null) { result = next.onRejected.call(null, args, false); } }
      
      





まず、標準では、前の結果を次の約束のプロセッサに転送することが義務付けられています(チェーンの主な意味は、操作を割り当ててから、2番目の操作を割り当てて、最初の結果を受け入れるようにすることです)。



第二に、エラーは特別な方法で処理されます。 成功した結果がチェーンに沿って(変更して)最後に送信されると、ハンドラーコードで発生したエラーは、次の拒否されるまで1ステップだけ送信されるか、チェーンの最後に到達するとポップアップします。



第三に、関数は新しいプロミスを返すことができます。 この場合、次のものが既に設定されている場合、既存のハンドラをスローすることにより、次のものを置き換える必要があります。 これにより、インスタント実行ハンドラーと非同期ハンドラーの組み合わせが可能になり、それら自体がPromiseを返します。



上記のコードは、これらすべてのシナリオに対応しています。



最初のテスト



  JSParser jp = new JSParser("function cbk(str) { \"Promise fulfilled: \" + str } function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 1500) }"); System.out.println(); System.out.println("function cbk(str) { \"Promise fulfilled: \" + str }"); System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 1500) }"); System.out.println(); Expression exp = Expression.create(jp.getHead()); exp.eval(); jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp); f.setSilent(true); jsparser.Promise p = new jsparser.Promise(f); p.then((jsparser.Function)Expression.getVar("cbk", exp));
      
      





これまで、すべてをJavaコードで管理してきました。 それにもかかわらず、すべてがすでに機能しています。1秒半で、コンソールに「約束が満たされました:OK」という碑文が表示されます。 ところで、Promiseのワーキング関数からチェーンなしで呼び出される解決関数と拒否関数は、任意の数の引数を取ることができます。 とても便利です。 この例では、文字列「OK」を渡しました。



別の小さなコメント:チェーン中に作成されたプロミスには、原則として機能する機能はありません。 前のプロミスの状態が変化すると、すぐにハンドラーを呼び出します。



例はもっと複雑です:



  JSParser jp = new JSParser("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str } " + "function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" } " + "function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str } " + "function err(str) { \"An error has occured: \" + str } " + "function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }"); System.out.println(); System.out.println("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str }"); System.out.println("function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" }"); System.out.println("function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str }"); System.out.println("function err(str) { \"An error has occured: \" + str }"); System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }"); System.out.println("(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)"); System.out.println(); Expression exp = Expression.create(jp.getHead()); ((jsparser.Function)Expression.getVar("f", exp)).setSilent(true); ((jsparser.Function)Expression.getVar("cbk2", exp)).setSilent(true); exp.eval(); jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp); f.setSilent(true); jsparser.Promise p = new jsparser.Promise(f); p.then((jsparser.Function)Expression.getVar("cbk1", exp)) .then((jsparser.Function)Expression.getVar("cbk2", exp)) .then((jsparser.Function)Expression.getVar("cbk3", exp), (jsparser.Function)Expression.getVar("err", exp));
      
      





この例を呼び出すと、次の出力が得られます。



{}

"Promise 1 fulfilled: OK"

"OK"

"An error has occured: ERROR"

undefined

"Promise 2 fulfilled: OK"







最初の中括弧は、チェーンの結果としてコールのチェーンが返されたpromiseオブジェクトです。 cbk1関数では、「OK」を返しました。この値はcbk2に渡され、最後の行に表示されています。 cbk2の内部では、値「ERROR」のエラーをスローします。したがって、cbk3は実行されませんが、errが実行されます(チェーン内の前のpromiseのハンドラーでエラーが発生した場合)。 しかし、このコードは即座に実行されますが、cbk2の出力はタイマーに掛けられた補助関数を介して行われます。 必要に応じてstr変数にアクセスできますが、出力は以下のとおりです。 この例をChrome 49で実行すると、1つの例外を除いてまったく同じ出力が得られます。変数strは、setTimeoutに渡された匿名関数では表示されません。 これは、Chromeの矢印関数の動作の特徴です(そして、おそらく標準に従って非常に必要です。ここでは、問題が何であるかを言うのが難しいと思います)。 矢印関数を通常の関数に変更すると、出力は同じになります。



Javascript probros



しかし、それだけではありません。 私たちの最終的な目標は、インタープリターによって実行されるJSコードが新しい機能を使用することです。 しかし、これはすでに技術の問題です。



コンストラクターを作成します。



 public class PromiseC extends Function { public PromiseC() { items.put("prototype", PromiseProto.getInstance()); PromiseProto.getInstance().set("constructor", this); } @Override public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) { return call(context, args); } @Override public JSValue call(JSObject context, Vector<JSValue> args) { if (args.size() == 0) return new Promise(null); if (!args.get(0).getType().equals("Function")) { JSError e = new JSError(null, "Type error: argument is not a function", getCaller().getStack()); getCaller().error = e; return new Promise(null); } return new Promise((Function)args.get(0)); } }
      
      





そして、必要なメソッドのセットを持つプロトタイプオブジェクト:



 public class PromiseProto extends JSObject { class thenFunction extends Function { @Override public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) { if (args.size() == 1 && args.get(0).getType().equals("Function")) { return ((Promise)context).then((Function)args.get(0)); } else if (args.size() > 1 && args.get(0).getType().equals("Function") && args.get(1).getType().equals("Function")) { return ((Promise)context).then((Function)args.get(0), (Function)args.get(1)); } else if (args.size() > 1 && args.get(0).getType().equals("null") && args.get(1).getType().equals("Function")) { return ((Promise)context)._catch((Function)args.get(1)); } return context; } } class catchFunction extends Function { @Override public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) { if (args.size() > 0 && args.get(0).getType().equals("Function")) { return ((Promise)context)._catch((Function)args.get(0)); } return context; } } private PromiseProto() { items.put("then", new thenFunction()); items.put("catch", new catchFunction()); } public static PromiseProto getInstance() { if (instance == null) { instance = new PromiseProto(); } return instance; } @Override public void set(JSString str, JSValue value) { set(str.getValue(), value); } @Override public void set(String str, JSValue value) { if (str.equals("constructor")) { super.set(str, value); } } @Override public String toString() { String result = ""; Set keys = items.keySet(); Iterator it = keys.iterator(); while (it.hasNext()) { if (result.length() > 0) result += ", "; String str = (String)it.next(); result += str + ": " + items.get(str).toString(); } return "{" + result + "}"; } @Override public String getType() { return type; } private String type = "Object"; private static PromiseProto instance = null; }
      
      





すべてが機能するように、最初にPromiseコンストラクターに1行追加することを忘れないでください。



  public Promise(Function f) { items.put("__proto__", PromiseProto.getInstance()); ... }
      
      





そして、テストを少し変更します。



  JSParser jp = new JSParser("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str } " + "function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" } " + "function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str } " + "function err(str) { \"An error has occured: \" + str } " + "function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }; " + "(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)"); System.out.println(); System.out.println("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str }"); System.out.println("function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" }"); System.out.println("function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str }"); System.out.println("function err(str) { \"An error has occured: \" + str }"); System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }"); System.out.println("(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)"); System.out.println(); Expression exp = Expression.create(jp.getHead()); ((jsparser.Function)Expression.getVar("f", exp)).setSilent(true); ((jsparser.Function)Expression.getVar("cbk2", exp)).setSilent(true); exp.eval(); jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp); f.setSilent(true);
      
      





結論は変わらないはずです。



それだけです! すべてが正常に機能し、追加の単体テストを記述して、発生する可能性のあるエラーを検索できます。



このメカニズムをJavaに適応させる方法は? とても簡単です。 関数に似たクラスを作成し、operateメソッドで何かを行います。 そして、すでにラッパーでラップしています。 いずれにせよ、このテーマにはたくさんの素晴らしいパターンがあります。



この記事が誰かに役立つことを願っています。 エンジンソースを思いついたらすぐに投稿し、不足している機能を追加します。 良い一日を!



All Articles