web-dev-qa-db-ja.com

node.jsでネストされたプロミスは正常ですか?

Node.jsの学習中に2週間苦労してきた問題は、nodeを使用して同期プログラミングを行う方法です。どのようにしようとしてもsequentially常にネストされたプロミスになります。保守性に関する限り、プロミスチェーンを支援するQなどのモジュールがあることがわかりました。

調査中に理解できないのは、Promise.all()Promise.resolve()、およびPromise.reject()です。 Promise.rejectは名前からは自明ですが、アプリケーションを書くとき、アプリケーションの動作を壊さずにこれらを関数やオブジェクトに含める方法について混乱しています。

JavaやC#などのプログラミング言語から来る場合、node.jsの学習曲線は間違いなくあります。疑問点は、node.jsでプロミスチェーンが正常か(ベストプラクティスか)です。

例:

driver.get('https://website.com/login').then(function () {
    loginPage.login('company.admin', 'password').then(function () {
        var employeePage = new EmployeePage(driver.getDriver());

        employeePage.clickAddEmployee().then(function() {
            setTimeout(function() {
                var addEmployeeForm = new AddEmployeeForm(driver.getDriver());

                addEmployeeForm.insertUserName(employee.username).then(function() {
                    addEmployeeForm.insertFirstName(employee.firstName).then(function() {
                        addEmployeeForm.insertLastName(employee.lastName).then(function() {
                            addEmployeeForm.clickCreateEmployee().then(function() {
                                employeePage.searchEmployee(employee);
                            });
                        });
                    });
                });
            }, 750);
        });
    });
});
56
Grim

いいえ、Promisesの大きな利点の1つは、非同期コードをネストではなく線形に保つことができることです(継続渡しスタイルからのコールバック地獄)。

Promiseは、ステートメントを返し、エラーをスローすることを提供します。これは、継続渡しスタイルでは失われます。

非同期関数からプロミスを返す必要があるので、返された値にチェーンできます。

以下に例を示します。

driver.get('https://website.com/login')
  .then(function() {
    return loginPage.login('company.admin', 'password')
  })
  .then(function() {
    var employeePage = new EmployeePage(driver.getDriver());
    return employeePage.clickAddEmployee();
  })
  .then(function() {
    setTimeout(function() {
      var addEmployeeForm = new AddEmployeeForm(driver.getDriver());

      addEmployeeForm.insertUserName(employee.username)
        .then(function() {
          return addEmployeeForm.insertFirstName(employee.firstName)
        })
        .then(function() {
          return addEmployeeForm.insertLastName(employee.lastName)
        })
        .then(function() {
          return addEmployeeForm.clickCreateEmployee()
        })
        .then(function() {
          return employeePage.searchEmployee(employee)
        });
    }, 750);
});

Promise.allはpromiseの配列を取り、すべてのpromiseが解決されると解決します。いずれかが拒否された場合、配列は拒否されます。これにより、非同期コードをシリアルではなく並行して実行し、すべての並行機能の結果を待つことができます。スレッドモデルに慣れている場合は、スレッドを生成してから参加することを検討してください。

例:

addEmployeeForm.insertUserName(employee.username)
    .then(function() {
        // these two functions will be invoked immediately and resolve concurrently
        return Promise.all([
            addEmployeeForm.insertFirstName(employee.firstName),
            addEmployeeForm.insertLastName(employee.lastName)
        ])
    })
    // this will be invoked after both insertFirstName and insertLastName have succeeded
    .then(function() {
        return addEmployeeForm.clickCreateEmployee()
    })
    .then(function() {
        return employeePage.searchEmployee(employee)
    })
    // if an error arises anywhere in the chain this function will be invoked
    .catch(function(err){
        console.log(err)
    });

Promise.resolve()およびPromise.reject()は、Promiseを作成するときに使用されるメソッドです。これらはコールバックを使用して非同期関数をラップするために使用されるため、コールバックの代わりにPromiseを使用できます。

Resolveは、promiseを解決/実行します(これは、チェーンされたthenメソッドが結果の値で呼び出されることを意味します)。
Rejectはプロミスを拒否します(つまり、連鎖thenメソッドは呼び出されませんが、最初の連鎖catchメソッドは発生したエラー)。

プログラムの動作を維持するために、setTimeoutを残しましたが、おそらく不要です。

91
Tate Thurston

ネストされたチェーンの代わりに、asyncライブラリを使用し、async.seriesを使用します。

async.series([
    methodOne,
    methodTwo
], function (err, results) {
    // Here, results is the value from each function
    console.log(results);
});

Promise.all(iterable)メソッドは、反復可能な引数内のすべてのプロミスが解決したときに解決するプロミスを返すか、拒否された最初に渡されたプロミスの理由で拒否します。

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(function(values) { 
  console.log(values); // [3, 1337, "foo"] 
});

Promise.resolve(value)メソッドは、指定された値で解決されるPromiseオブジェクトを返します。値がthenableである(つまりthenメソッドがある)場合、返されるpromiseはそのthenableに「追従」し、最終的な状態を採用します。そうでない場合、返された約束は値で満たされます。

var p = Promise.resolve([1,2,3]);
p.then(function(v) {
  console.log(v[0]); // 1
});

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

8
Thalaivar

不要なネストを削除しました。 'bluebird'(私の好みのPromiseライブラリ)の構文を使用します http://bluebirdjs.com/docs/api-reference.html

var employeePage;

driver.get('https://website.com/login').then(function() {
    return loginPage.login('company.admin', 'password');
}).then(function() {
    employeePage = new EmployeePage(driver.getDriver());    
    return employeePage.clickAddEmployee();
}).then(function () {
    var deferred = Promise.pending();
    setTimeout(deferred.resolve,750);
    return deferred.promise;
}).then(function() {
    var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
    return Promise.all([addEmployeeForm.insertUserName(employee.username),
                        addEmployeeForm.insertFirstName(employee.firstName),
                        addEmployeeForm.insertLastName(employee.lastName)]);
}).then(function() {
    return addEmployeeForm.clickCreateEmployee();
}).then(function() {
    return employeePage.searchEmployee(employee);
}).catch(console.log);

すべての質問の例を含めるようにコードを変更しました。

  1. Promiseを使用する場合、非同期ライブラリを使用する必要はありません。約束はそれ自体非常に強力であり、約束と非同期のようなライブラリを混在させるアンチパターンだと思います。

  2. 通常、var deferred = Promise.pending()スタイルの使用は避けてください...

'標準の規則に従っていないコールバックAPIをラップする場合。 setTimeout: 'と同様

https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns

SetTimeoutの例では、「遅延」Promiseを作成します。setTimeout内のPromiseを解決し、setTimeoutの外でPromiseを返します。これは少し直感的でないように見えるかもしれません。この例を見て、別の質問に答えました。 Q.jsはノードで約束します。`socket`にエラーハンドラがありません。TypeError:未定義のメソッド 'then'を呼び出すことはできません

通常、Promise.promisify(someFunction)を使用して、コールバック型の関数をPromiseの戻り関数に変換できます。

  1. Promise.all非同期に戻るサービスに対して複数の呼び出しを行っているとしましょう。互いに依存していない場合は、同時に呼び出しを行うことができます。

関数呼び出しを配列として渡すだけです。 Promise.all([promiseReturningCall1、promiseReturningCall2、promiseReturningCall3]);

  1. 最後に、最後にcatchブロックを追加します。エラーを確実にキャッチします。これにより、チェーン内の任意の例外がキャッチされます。
3

類似の質問 に答えたところです。ここでは、ジェネレーターを使用してPromiseチェーンをニースの方法で平坦化する手法を説明しました。この手法は、コルーチンからインスピレーションを得ています。

このコードを取ります

Promise.prototype.bind = Promise.prototype.then;

const coro = g => {
  const next = x => {
    let {done, value} = g.next(x);
    return done ? value : value.bind(next);
  }
  return next();
};

これを使用すると、深くネストされたPromiseチェーンをこれに変換できます

coro(function* () {
  yield driver.get('https://website.com/login')
  yield loginPage.login('company.admin', 'password');
  var employeePage = new EmployeePage(driver.getDriver());
  yield employeePage.clickAddEmployee();
  setTimeout(() => {
    coro(function* () {
      var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
      yield addEmployeeForm.insertUserName(employee.username);
      yield addEmployeeForm.insertFirstName(employee.firstName);
      yield addEmployeeForm.insertLastName(employee.lastName);
      yield addEmployeeForm.clickCreateEmployee();
      yield employeePage.searchEmployee(employee);
    }());
  }, 750);
}());

名前付きジェネレーターを使用して、さらに読みやすくすることができます

// don't forget to assign your free variables
// var driver = ...
// var loginPage = ...
// var employeePage = new EmployeePage(driver.getDriver());
// var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
// var employee = ...

function* createEmployee () {
  yield addEmployeeForm.insertUserName(employee.username);
  yield addEmployeeForm.insertFirstName(employee.firstName);
  yield addEmployeeForm.insertLastName(employee.lastName);
  yield addEmployeeForm.clickCreateEmployee();
  yield employeePage.searchEmployee(employee);
}

function* login () {
  yield driver.get('https://website.com/login')
  yield loginPage.login('company.admin', 'password');
  yield employeePage.clickAddEmployee();
  setTimeout(() => coro(createEmployee()), 750);
}

coro(login());

ただし、これは約束の流れを制御するためにコルーチンを使用して可能なことの表面をひっかくだけです。この手法の他の利点と機能のいくつかを示す、上記でリンクした回答をお読みください。

この目的でコルーチンを使用する場合は、 co library を確認することをお勧めします。

お役に立てれば。

PSsetTimeoutをこの方法で使用している理由がわかりません。具体的には750ミリ秒待機するポイントは何ですか?

2
user633183

次のステップは、ネストからチェーンへの移行です。各プロミスは、親プロミスにチェーンできる孤立したプロミスであることを認識する必要があります。つまり、チェーンへの約束を平坦化できます。各promiseの結果は、次の結果に渡すことができます。

それについての素晴らしいブログ投稿があります: Flattening Promise Chains 。 Angularを使用しますが、それを無視して、Promiseの深いネストがチェーンにどのように変化するかを確認できます。

もう1つの良い答えは、StackOverflowについてです: javascriptの約束を理解する;スタックとチェーン

1
Cymen

次のようにプロミスを連鎖できます:

driver.get('https://website.com/login').then(function () {
    return loginPage.login('company.admin', 'password')
)}.then(function () {
    var employeePage = new EmployeePage(driver.getDriver());

    return employeePage.clickAddEmployee().then(function() {
        setTimeout(function() {
            var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
        return addEmployeeForm.insertUserName(employee.username).then(function() {
                retun addEmployeeForm.insertFirstName(employee.firstName)
         }).then(function() {
                return addEmployeeForm.insertLastName(employee.lastName)
         }).then(function() {
             return addEmployeeForm.clickCreateEmployee()
         }).then(function () {
             retrun employeePage.searchEmployee(employee);
        })}, 750);
});

}); });

0
tubu13