2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【JavaScript】非同期処理について 3 〜Promiseチェーンについて〜

Last updated at Posted at 2021-01-01

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

###注意
###本記事には、各種記事を調べた結果の自分なりの考察や推論が入っていますので、特に間違っている可能性が高いです。そのことを踏まえた上で参考にしていただけると幸いです。

本記事は下記の記事の続きとなります。
[【JavaScript】非同期処理について1〜タスクキュー、コールスタック、イベントループ〜]
(https://qiita.com/sho_U/items/ff82aa576837198097ce)
[【JavaScript】非同期処理について 2〜非同期処理のチェーン、コールバックヘル〜]
(https://qiita.com/sho_U/items/313df6fae06bf61bb58d)

また、クラスやインスタンス化の話もでてくるので、こちらの記事も参考になるかもしれません。
[【JavaScript】クラス構文について1]
(dhttps://qiita.com/sho_U/items/aaf31fad5394edb70861)

#Promiseとは

コールバックヘルのようにコードの見通しが悪くなることを避けるため、ES6から導入された、「より簡単に」、「より可読性高く」、非同期処理を書けるようにした記法

#Promiseのインスタンス化

Promiseを利用するには、インスタンス化する必要があります。

promise = new Promise();

インスタンス化する際に、コンストラクタに関数を渡すと実行されます。

new Promise( ()=>{console.log("hello")} );

コンストラクタに与えられた関数は、コールスタックに積まれて同期的に実行されます。

console.log("a");

new Promise(() => { console.log("b") });

console.log("c");

//a
//b
//c

コンストラクタに与えられた関数は非同期で実行されるわけではないのですね。

#Promiseの内部状態

インスタンス化されたpromiseオブジェクトは3つの内部状態を持ちます。
ざっくりとしたイメージは下記の通りです。

pending : 初期状態。(処理結果保留状態)
fulfilled : 処理が成功した。
rejected : 処理が失敗した。

インスタンス化した直後は、処理の結果はでていないので、"pending" となります。
イメージとしては下記のような状態です。

promise = { //promise は Promiseをnewでインスタンス化したもの。
  state : "pending" 
  }

上記の例では、console.logを実行しただけなので、そのまま関数を渡しましたが、Promiseを用いて非同期処理を実施するには、ある二つの関数を、引数に渡します。
関数の引数に関数を渡すで分かりづらいですが、下記のようになります。

new Promise( (関数1, 関数2)=>{ 処理 } );

関数1は、**「resolve」という名の、関数2には「reject」**という名の関数が入ります。

new Promise( (resolve, reject)=>{ 非同期処理 } );

処理の中でresolveが実行されると、promiseインスタンスオブジェクトの状態は,"fulfilled"となります。
処理の中でrejectが実行される。もしくは例外が生じた場合は
promiseインスタンスの状態は,**"rejected"**となります。

どういうことかというと例えば

consst promise = new Promise( (resolve, reject)=>{ 
   resolve(); 
 } );

上記のように、resolve関数が実行された場合。promiseインスタンスオブジェクトの中身は以下のようなイメージとなります。

promise = {
  state: "fulfilled" //pendingからfulfilledに変化
  }

なお、状態が一度pendingからfulfilledもしくはrejectedに変更した場合、以降状態が変更することはありません。

#非同期処理の結果を取り出す

promiseインスタンスオブジェクトから処理の結果を取り出すには **「thenメソッド」「catchメソッド」**を使用します。

#thenメソッドについて

thenメソッドはざっくりというと、処理が成功した場合と、失敗した場合に、それぞれ実行される関数を登録するためのメソッドです。
thenメソッドの第一引数には、成功時の処理が。
thenメソッドの第二引数には、失敗時の処理が、それぞれ設定されます。

成功時というのは、関数resolveが呼び出された時です。また、関数resolveが呼び出されるとpromiseインスタンスオブジェクトの状態が**"fulfilled"になります。
失敗時というのは、関数rejectが呼び出された時、または
例外が発生した時です。また、関数rejectが呼び出される、もしくは例外が発生するとpromiseインスタンスオブジェクトの状態"rejected"**になります。

promise = new Promise((resolve, reject)=>{
  //非同期処理
  }

promise.then("関数resolveの処理", "関数rejectの処理")

もし、第一引数が関数でなければ、入力値をそのまま返す関数が代わりに使われます。
第二引数が関数でなければ、入力値を例外として投げる関数が代わりに使われます。

今、下記のようなプログラムがあるとします。Promiseにコンストラクタとして渡される関数halfは擬似的な50%の確率で成功するプログラムです。

function half(resolve, reject) {
  (Math.floor(Math.random() * 100) >= 50) ? resolve("OK") : reject("NG"); //2
}

const promise = new Promise(half);//1

promise.then((msg) => { console.log("成功" + msg) }, (msg) => { console.log("失敗" + msg) });

IMG_98F5A4AEC2CF-1.jpeg

thenの第一引数、第二引数にそれぞれ関数を定義しているので、イメージとして下記のように登録した感じです。

イメージ.js
const resolve = (msg) => { console.log("成功" + msg)};
const reject = (msg) => { console.log("失敗" + msg) });

処理が成功して、"OK"が出力されるまでの処理の流れを見てみます。

//1 まず、Promiseがインスタンス化されます。コンストラクタには関数halfが渡されます。
この時、promiseインスタンスオブジェクトの状態は**"pending"**となります。

イメージ.js
promise = {
  state: "pending"
  }

//2 インスタンス化されたら、コンストラクタに渡された関数halfは同期的にコールスタックに積まれるため、実行されます。
ランダムに生成された数値が50以上だった場合、resolve("OK")が呼び出され、promiseインスタンスオブジェクトの状態は**"fulfilled"**となります。

関数resolveが呼び出された場合の処理は、thenメソッドの第一引数に登録されているので、下記の処理が実行されます。

(msg) => { console.log("成功" + msg)}; //msgには"OK"が渡される。

###catchメソッドについて

cathcメソッドは、rejectの処理内容を登録しておきます。つまり、thenの第二引数に定義した内容をcatchメソッドの引数に定義できます。

function half(resolve, reject) {
  (Math.floor(Math.random() * 100) >= 50) ? resolve("OK") : reject("NG");
}

const promise = new Promise(half);

promise
.then((msg) => { console.log("成功" + msg) })
.catch((msg) => { console.log("失敗" + msg) }));

catchメソッドは、thenメソッドのシュガーシンタックスです。(同じ処理を異なる書き方で表現)
thenメソッドの第一引数にundefindeを設定した場合と同意となります。


.then(undefined, エラー処理関数)
.catch(エラー処理関数)

//等しい

なお、thenメソッドやchachメソッドに登録された関数が呼び出されるのは、promiseオブジェクトの状態が変わった時のみです。また、promiseオブジェクトの状態一度変更した場合、以降は不変となります。
よって、下記のように

resolve()
reject()

と呼び出された場合、thenメソッドの第二引数の関数、もしくはcatchメソッドに登録した関数が呼び出されることはありません。
なぜなら、resolveが呼び出されて時点で、prmiseインスタンスオブジェクトの状態は"fulfilled"となるため、次にrejectを呼び出したとしても状態が変更することはないので、thenメソッド及びcatchメソッドに登録された関数が呼び出されることはないのです。

当然以下のようにresolveメソッドを連続で呼び出しても、thenメソッドで呼び出されるは最初の一回だけです。

resolve()  //状態がpendingからfulfilledに変更
resolve()  //fulfilledから変更なし。

また、new Promiseをした後、特に処理をせずに、.thenメソッド呼び出す場合は、以下の様になる思います。

new Promise((resolve)=>{
  resolve()
}).then(console.log("ok"));
//Promiseのインスタンスを作成し、即resolveを呼び出している。

これは、**「Promise.resolveメソッド」**で省略できます。

Promise.resolve()
//Promiseインスタンスオブジェクトが生成され、resolveメソッドが実行される。

よって、

new Promise((resolve)=>{
  resolve("hello")
}).then((n)=>{console.log(n)});

Promise.resolve("hello").then((n)=>{console.log(n)});

上の式と、下の式は同意となります。

また、promise.rejectメソッドは、promise.resoloveメソッドと同様の使い方をし、この場合は、rejectメソッドを実行するので、then(undefined, 関数)やcatch(関数)を呼び出すことができます。

Promise.reject().catch(console.log("エラー");

上記に、new Prmise時にコンストラクタとして設定された関数は同期的に実行されると説明しましたが、thenやcatchで登録されたメソッドは非同期的に実行されます。

この場合、同期に実行されるとは呼び出し時にコールスタックに積まれることで、非同期に実行されるとは、一旦タスクキューに登録されるということです。

この辺の詳細は、こちらの記事をご覧ください。
[【JavaScript】非同期処理について1〜タスクキュー、コールスタック、イベントループ〜]
(https://qiita.com/sho_U/items/ff82aa576837198097ce)

例えば以下の様なプログラムがあるとします。

const promise = new Promise((resolve) => {
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then((value) => {
    console.log(value); // 3
});
console.log("outer promise"); // 2

まず、最初に1でPromiseがインスタンス化されて、コンストラクタに与えられた関数がコールスタックに積まれ、実行されて、コールスタックから追い出されます。
次に、resolve(42)が実行されるので、thenメソッドが呼ばれます。thenメソッドに引数として与えられている関数は、タスクキューに積まれます。
2の処理がコールスタックに積まれ、実行されて、コールスタックから追い出されます。
コールスタックが空になったので、タスクキューで順番待ちをしている処理【(value) => {console.log(value)})】がコールスタックに積まれて実行されます。

よって、出力の順番は

inner promise
outer promise
42

となります。

#Promiseチェーンについて

const promise = Promise.resolve();
promise.then(taskA).then(taskB).then(finalTask)

Promiseチェーンとは上記の様な形で、thenやcatchメソッドを数珠繋ぎでつないでいくことです。こうすることで、コールバッックヘルを回避して、連続的な非同期処理を記載することができます。

###ここで、非常に重要なことは、なぜ 「thenチェーン」 と言わずに、 「promiseチェーン」 と呼ぶのかということです。

繋いでいるのは、thenではありません。あくまでPromiseです。

thenは単にコールバックとなる関数を登録するだけではなく、 受け取った値を元に別のPromiseオブジェクトを生成します。

イメージとしては

promise.then(taskA).then(taskB).then(finalTask)
//最初のpromiseインスタンスから関数resolveが呼び出されて、taskAが実行される。taskAは新たにPromiseオブジェクトを返す。
promise.promise.then(taskB).then(finalTask)
//新たに返されたpromiseオブジェクトが.thenに登録されているtaskBを実行する。
//以降順次同様に連鎖してく。

もう少し詳しく見ていきます。
今、下記の様なプログラムがあるとします。

const promise = new Promise((resolve) => { //1
  resolve(1);
})

promise
  .then((n) => { //2
    console.log(n) //3
    return n + 1; //4
  })
  .then((n) => { //5
    console.log(n)
    return n + 1;
  })
  .then((n) => {
    console.log(n)
    return n + 1;
  })

//1が実行されて、最初の then //2 が呼び出されます。
//3 で "1"が出力されます。
//4で"2"が返されます。
そして、thenメソッドがPromiseインスタンスオブジェクトを作成します。この時の作成は内部的に

Promise.resolve(val)
//valは前のthenが返した値  この場合は"2"

もしくは

new Promise((resolve)=>{
  resolve(val)
}
//valは前のthenが返した値 この場合は"2"

の形で作成されます。

resolveが呼び出されたで、新たに作られたpromiseインスタンスオブジェクトの状態はpendingからfulfillとなり、thenメソッドに登録された関数//5が呼び出されます。
この様にして、連鎖していきます。

繰り返しになりますが、thenが次のthenにreturnで値を渡しているのではなく、thenがreturnした値を元に、新たにPromiseオブジェクトを作成して、そのPromiseオブジェクトが、thenメソッドに登録された関数を呼び出すということです。

では、thenメソッドに非同期処理を登録してみます。

手始めに以下の様なプログラムを実行してみます。
これは、1秒ごとに数字が加算されて出力されることを期待するプログラムです。

function sleep(val) {
  setTimeout(function () {
    console.log(val);
  }, 1000);
  let nextVal = val + 1;
  return nextVal
}

const promise = new Promise((resolve) => {
  resolve(1);
});

promise
  .then((val) => { sleep(val) })
  .then((val) => { sleep(val) })
  .then((val) => { sleep(val) })

結果は、下記の様になります。

1
undefined
undefined

問題点

  1. 後続の処理がundefinedが出力されている
  2. 1秒経過後、全て同時に出力される。

問題点1の原因
最初のthenでは、resolve呼び出し時の引数1が渡って来ます。よって1が正しく出力されます。
しかし、呼び出されたsleep関数自体は、return で"2"を返しますが、thenに登録している関数自体は"2"を受け取っているだけで、"2"を返していない。つまり返り値がないからです。
あくまで、次のthenに登録された関数に渡る値は、前のthenに登録された関数が返す値となります。
今、最初のthenに登録されている関数は、下記の様になっています。

(val) => { 2 }

これを

(val) => { return 2 }

というふうに、thenに登録された関数自体が値を返す様にしなければ、次のthenに渡っていきません。
以下のように修正してみます。

function sleep(val) {
  setTimeout(function () {
    console.log(val);
  }, 1000);
  let nextVal = val + 1;
  return nextVal
}

const promise = new Promise((resolve) => {
  resolve(1);
});

promise
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })

実行してみます。
結果は、下記の様になります。

1
2
3

問題点1は解決されました。

問題点2の原因
先ほど、thenは登録されている関数の返り値からPromiseインスタンスオブジェクトを生成すると説明しました。
このPromiseオブジェクトは、作成されると同時にresolveが呼び出され、状態がfulfilledになるので、setTimeoutの非同期処理結果を待たずして、次のthenに登録されている関数が呼び出されてしまいます。
ですでの、全て同時に出力されてしまうということです。

thenは登録されている関数の返り値からPromiseインスタンスオブジェクトを生成すると説明しましが、例外があります。

それは、登録されている関数の返り値が、そもそもPromiseオブジェクトだった場合です。その場合は、そのまま、そのPromiseオブジェクトがthenを呼びことになります。
その場合、返されたPromiseオブジェクトの状態がpendingだった場合は、thenに登録されている関数は実行されず、状態がfulfiledもしくはrejectedに変更された場合に、thenメソッドに登録の関数が実行されます。
以下の様にsleep関数はPromiseオブジェクトを返す形に修正してみます。

function sleep(val) {
  return new Promise((resolve) => { //1
    setTimeout(function () {
      console.log(val);
      resolve(val + 1);
    }, 1000);
  })
}

const promise = new Promise((resolve) => {
  resolve(1);
});

promise
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })

実行してみます。

1
2
3

求める結果となりました。

処理を見ていきます。

まず1が実行されて、Promiseインスタンスオブジェクトが作成されます。
登録された関数が実行されて、setTimeout関数が実行されます。
この関数はざっくりいうと「1秒後に、ログ出力とresolveを実行してね」と登録する関数です。
そして、作成されたPromiseインスタンスオブジェクトがreturnされます。
この、retrurnされたPromiseインスタンスオブジェクトは1秒経過するまで、状態はpendingとなります。

よって最初のthenに登録された関数の返り値はpending状態のPromiseオブジェクトとなります。

promise
  .then("pending状態のPromiseオブジェクト")
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })

そして、1秒経過後、resolveが呼び出されるので(ログ出力と合わせて)、fulfiled状態となります。
よって、そのPrmiseオブジェクトのthenメソッド(次のthenメソッド)が呼び出されます。
この様にして、非同期処理の順番を担保しながらPromiseチェーンを繋げることが可能となります。

最後に、前回の記事で登場したコールバックヘル状態のプログラムをPromiseを用いて書き直してみます。

コールバックヘル.js
function sleep(callback, n) {
  setTimeout(function () {
    console.log(n);
    n++
    callback(n);
  }, 1000);
}
sleep(function (n) {
  sleep(function (n) {
    sleep(function (n) {
      sleep(function (n) {
        sleep(function (n) {
          sleep(function (n) {
          }, n);
        }, n);
      }, n);
    }, n);
  }, n);
}, 0);
Promise.js
function sleep(val) {
  return new Promise((resolve) => { //1
    setTimeout(function () {
      console.log(val);
      resolve(val + 1);
    }, 1000);
  })
}

const promise = Promise.resolve(0);

promise
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })
  .then((val) => { return sleep(val) })

処理の見通しが、だいぶ良くなったのが見て取れます。
※なお今回は、説明を分かりやすくするために同じ関数sleepを呼び出しているため、反復処理で、より簡潔に記載することは可能です。

冒頭でも説明しましたが、今回の記事は、自分の考察が多分に含まれているため、間違っている可能性があります。
投稿においては、下記の記事を参考にしましたので正確な情報は、そちらを参考にしていただけると幸いです。

また、下記の記事でより詳細に非同期処理を記載いたしました。
もしよければ、下記の記事もご参考ください。

###参考文献

[JavaScript Primer非同期処理:コールバック/Promise/Async Function]
(https://jsprimer.net/basic/async/)
[さくらのナレッジ JavaScriptの非同期処理を理解する その2 〜Promise編〜]
(https://knowledge.sakura.ad.jp/24890/)
[【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた]
(https://affi-sapo-sv.com/note/js-promise.php#title5)
[JavaScript Promiseの本]
(https://azu.github.io/promises-book/)
udemy 【JS】初級者から中級者になるためのJavaScriptメカニズム
JavaScript 本格入門 著.山田 祥寛

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?