39
52

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】Promiseくんと仲良くなろう

Last updated at Posted at 2020-09-10

背景

JSを始めると必ずぶち当たる「Promiseの壁」
ググってもよく分からなかったので自分なりにまとめてみようと思った

こんな人向け

  • 最近JS始めたよ
  • とりあえずawait付けとけってじっちゃが言ってた
  • Promise何となく使ってるけど、いつ事故を起こすかビクビクしている
  • Promise完全に理解した
  • 茶番が好き

上記は全て著者のことですが、一旦全て棚に上げさせてください(そうしないと記事が自虐だけで終わってしまいます)

注意

当記事は8割が茶番で構成されております。
真面目な記事をお探しの方は即座にブラウザバックされることを推奨いたします。

本編

1. そもそも非同期処理って?

相手と仲良くなるには、まず相手のことを知ることが重要です。まずは基本から。
JavaScript Primer
より引用

非同期処理はコードを順番に処理していきますが、ひとつの非同期処理が終わるのを待たずに次の処理を評価します。 つまり、非同期処理では同時に実行している処理が複数あります。

[同期処理]      [非同期処理]
処理A[2s]       処理A[2s]      
   ⬇︎              ⬇︎
処理B[1s]       処理B[1s]

[結果]          [結果]
処理A出力     処理B出力
処理B出力        処理A出力
計3[s]          計2[s]

まぁ、書いてある通りですね。(説明するとこなかった)

JavaScriptではその非同期処理をPromiseという形で扱ってるってことですね。

2. 教えて!Promiseくん

次にPromiseくんについて詳しくみていきます。転校生への質問責めのごとく、根掘り葉掘り聞いていきましょう。(リアルでやると引かれます)

何ができるの?(ぶしつけ)

非同期処理が作れるよ(いきなり失礼だなコイツ).js
const p = new Promise(() => {
  console.log('Promise!!!');
});

p;

// "Promise!!!"

どうやら、new Promise() と書いてあげると非同期処理が作れるみたいです。

スゴイネ。

試しにそれっぽいことやってみせてよ(無茶振り)

えぇ・・・じゃあちょっとだけ(困惑).js
const p = new Promise(() => {
  setTimeout(() => {
    console.log('Promise!!!')
  }, 1000);
});

p;
console.log('同期処理');

// "同期処理"
// "Promise!!!"

SetTimeout()は第1引数に渡した処理を第2引数[ms]後に実行する関数です。
それを非同期処理にしているので、後から実行した同期処理が先に出力されていますね。

おぉ、何だかそれっぽい!
(次の話題は何にしようか・・・、あ!)

もしかしてそれ"HIDOKIES"のキーホルダー?欲しいな〜(強欲)

いいよ。投げるからちゃんとキャッチしてね(少しこらしめてやるか).js
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log('1s経過');
    resolve('o---o[HIDOKIES]');
  }, 1000);
});

console.log('👋', p);

// "👋" [object Promise]{}
// "1秒経過"

放物線を描き、1秒後にHIDOKIESのキーホルダーが手に納まる━━━━━
ハズが、Promiseくんの手を離れた瞬間に[[object Promise]{}]になって、手の中に瞬間移動していました。

え、ナニコレ、こっわ!!Promniseくんってマジシャン!?
てか、なにこの塊。キーホルダーの原型ないじゃん・・・

調子に乗ってすみませんでした。タネも仕掛けも教えてください・・・

しょうがないなぁ(愉悦)言う通りに構えてみて.js
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log('1s経過');
    resolve('o---o[HIDOKIES]');
  }, 1000);
});

// then()で構えたら受け取れるよ
p.then(v => {
  console.log('👋', v);
});

// 1s経過
// "👋" "o---o[HIDOKIES]"

Promiseくんの言う通りに手を構えるとあら不思議。1秒後にちゃんとキーホルダーが受け取れました。
彼云く、「PromiseはPromiseオブジェクトを返すから、thenで結果を受け取ってあげる必要がある」とのこと。「分かっている人の話ほど分からない理論」ですね :thinking:

わかるように説明してよ(怒)

[mozilla-Promise] (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)より引用

Promise インターフェイスは作成時点では分からなくてもよい値へのプロキシです。 Promise を用いることで、非同期アクションの成功や失敗に対するハンドラーを関連付けることができます。これにより、非同期メソッドは、最終的な値を返すのではなく、未来のある時点で値を持つ Promise を返すことで、同期メソッドと同じように値を返すことができるようになります。

要は、Promise自体の戻り値は結果が未定のまま即座にPromiseオブジェクトという形で返されますよ。ってことです。
さっきの[object Promise]{}はPromiseオブジェクトそのもので、当然この時点では結果が未定なわけです。
では、未来に結果が確定したときどうやって受け取るのでしょう?

続けて引用

Promise の状態は以下のいずれかとなります。

  • 待機pending: 初期状態。成功も失敗もしていません。
  • 満足fulfilled: 処理が成功して完了したことを意味します。
  • 拒絶rejected: 処理が失敗したことを意味します。
    待機状態のプロミスは、何らかの値を持つ満足 (fulfilled) 状態、もしくは何らかの理由 (エラー) を持つ拒絶 (rejected) 状態のいずれかに変わります。

うわ・・・難しそうな単語の羅列・・・。難しそうな単語アレルギーが出そうです。

先ほどPromiseくんがキーホルダーを投げる時に、resolve()を使っていたのを覚えていますでしょうか?(伏線張るの下手くそかよ
これは、Promiseオブジェクトを「満足」状態に移行させる、つまり、ここで結果が確定するわけですね。
reject()なるものが対になって存在していますが、「拒絶」状態にするだけで結果を確定させることに違いはありません。

     ┌> resolve() -> 満足(成功)
待機 ┤
     └> reject() -> 拒絶(失敗)

うーん、なんとなく分かった気がする・・・

いいなぁ〜HIDOKIESのキーホルダー、私たちにもちょうだ〜い!(便乗)

まだいくつかあるからいいよ(なんなのこのクラスの奴ら).js
const keyholderList = ['o---o[HIDOKIES1]', 'o---o[HIDOKIES2]', 'o---o[HIDOKIES3]'];

// 関数化することで呼ばれるたびに新しいPromiseを生成します
// 一度「結果が確定」したPromiseは何度呼び出しても結果は「確定したまま」です
const throwKeyholder = ()  => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const keyholder = keyholderList.pop();
      if(keyholder) resolve(keyholder);
      else reject('ごめんね。もうキーホルダーなくなっちゃった。');
    }, 1000);
  });
};

throwKeyholder().then(v => {
  console.log('👋A', v);
});
throwKeyholder().then(v => {
  console.log('👋B', v);
});
throwKeyholder().then(v => {
  console.log('👋C', v);
});
throwKeyholder().then(v => {
  console.log('👋D', v);
});

// "👋A" "o---o[HIDOKIES3]"
// "👋B" "o---o[HIDOKIES2]"
// "👋C" "o---o[HIDOKIES1]"

A、B、Cの3人が喜んでいる中、Dが怒り出しました。「どうして私のこと無視するのよ!」
それに対して、Promiseくんは「ちゃんとキーホルダーはもうないよって言ったよ?」の一点張り。

さて、悪いのはどちらでしょうか?
正解はDです。なぜなら、Promiseくんはちゃんとreject()でキーホルダーがない旨をあやまっているからです。

どうしてDさんは聞き取れなかったの?:ear:

聞く耳を持ってなかったからでは?(皮肉)これでどう?.js
const keyholderList = ['o---o[HIDOKIES1]', 'o---o[HIDOKIES2]', 'o---o[HIDOKIES3]'];

const throwKeyholder = ()  => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const keyholder = keyholderList.pop();
      if(keyholder) resolve(keyholder);
      else reject('ごめんね。もうキーホルダーなくなっちゃった。');
    }, 1000);
  });
};

throwKeyholder().then(v => {
  console.log('👋A', v);
});
throwKeyholder().then(v => {
  console.log('👋B', v);
});
throwKeyholder().then(v => {
  console.log('👋C', v);
});
throwKeyholder().then(v => {
  console.log('👋D', v);
}, e => {
  console.log('👂D', e);
});

// "👋A" "o---o[HIDOKIES3]"
// "👋B" "o---o[HIDOKIES2]"
// "👋C" "o---o[HIDOKIES1]"
// "👂D" "ごめんね。もうキーホルダーなくなっちゃった。"

今度はちゃんと「聞く耳」を持っていたので聞こえましたね!めでたしめでたし。
then()reject()された時の動作も定義できるんです。
ただ、reject()された時の動作はcatch()で定義するほうが一般的です。

catch.js
const p = new Promise((resolve, reject) => {
  reject('rejectされたよ');
});

p.then(v => {
  console.log("v", v);
}).catch(e => {
  console.log("e", e);
});

// "e", "rejectされたよ"

3. Promiseくんってawaitさんってまさか・・・?

なんだかんだでクラスに馴染めた(?)Promiseくんでしたが、「async教室でawaitさんといつも一緒にいるし、付き合っているのでは?」という噂が広まっていました。気になったので、本人に直接聞いてみることに。

awaitさんといつも一緒にいるけどなんで?

awaitと一緒だと同期処理になれるんだ.js
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log('1s経過');
    resolve('Promise!!!');
  }, 1000);
});

// awaitはasync付き関数内でしか使えません
const f = async() => {
  await p.then(v => {
    console.log("v", v);
  });
  console.log('同期処理');
};

f();

// "1秒経過"
// "v" "Promise!!!"
// "同期処理"

非同期処理のはずがawaitのおかげで結果が確定するまで待ってくれているようです。
非同期処理であるPromiseくんが同期処理になれるとはこういうことだったんですね。

えっと・・・それだけ?

他にもいいことがあるんだ.js
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log('1s経過');
    resolve('Promise!!!');
  }, 1000);
});

const f = async() => {
  console.log(await p);  // 注目
  console.log('同期処理');
};

f();

// "1秒経過"
// "Promise!!!"
// "同期処理"

awaitさんと一緒の時は、then()がなくても結果が受け取れています。
いちいちthen()を書く手間が省けます。これはハッピーですね。

あれ?じゃあ、rejectされたときはどうすんのさ

別に普段と何も変わらないよ.js
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('1s経過');
    reject('rejectしたよ');
  }, 1000);
});

const f = async() => {
  await p.catch(e => {
    console.log('e', e);
  });
  console.log('同期処理');
};

f();


// "1秒経過"
// "e" "rejectしたよ"
// "同期処理"

rejectを受け取る時はawaitさんが居ても居なくても同じみたいです。
resolveされた値のみが戻り値として受け取れるんですね。

そういえばasync教室でしか一緒にいないみたいだけどなんで?

そういうルールなんだ.js

え、そんだけ!?理由とかないの?解説記事としてどうなのそれ

色々調べてみましたがよくわからず・・・。見つかった文章は以下

awaitは非同期関数内(async付き関数)でしか使えません

じゃあさ、そもそもasyncってなんなの?

「俺自身」かな(キリッ).js
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log('1s経過');
    resolve('Promise!!!');
  }, 1000);
});

const f = async() => {
  console.log(await p);
  console.log('同期処理1');
};

f();
console.log('同期処理2');

// "同期処理2" <-- 注目
// "1秒経過"
// "Promise!!!"
// "同期処理1"

Promiseくんって実は痛い人なんじゃ・・・
あれ?「同期処理2」が先に実行されている!?

そうです。実はasyncを付けた関数は非同期処理になる、つまりはPromiseくんと同じというわけです。
ただの痛い人ではなかったんですね〜(痛くないとは言っていない)

もしかして、new Promise()をしなくても非同期処理は作れるの?

そう、asyncならね.js
const p = async() => {
  return 'Promise!!!';
};

p().then(v => {
  console.log('v', v);
});

// "v" "Promise!!!"

一つ質問いいかな、resolve()どこ行った?
君のような勘のいいガキは嫌いだよ(条件反射)

new Promise()の時はresolve()で「満足」状態に移行していましたが、asyncになった途端returnに入れ変わっています。
「入れ替わっている」というのは少し違うかも知れません。というのも、async付きとはいえ関数ですからreturnで値を返すのは当然ですね?
async関数の戻り値はPromiseオブジェクトですので、new Promise()で書き直すと以下になります。

長ったらしいじゃねぇかよ.js
const p = () => {
  return new Promise((resolve) => {
    resolve('Promise!!!');
  });
};

p().then(v => {
  console.log('v', v);
});

// "v" "Promise!!!"

同じことしてるハズなのにnew Promise()の方が長いじゃん:thinking:
上手にasyncを使うことで、コードを短く書けるってことですね。
個人的には、 なるべくasyncを選ぶことをお勧めします。関数として切り出す方が扱いやすいですし、中でawaitが使えてネストが減らせますしね。

そうはいってもrejectできないんじゃね?

できるんだな〜それが.js
const p = async() => {
  throw('reject');
};

p().catch(e => {
  console.log(e);
});

// "reject"

throw()は「例外」、簡単に言うとエラーを返す関数です。これによって「拒絶」状態に移行しているわけです。

結局awaitさんと付き合ってるの?

const wait = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const p = async(c, ms) => {
  await wait(ms);
  console.log(c);
};

Promise.all([
  p('imagination', 8), p('will', 2),
  p('up', 5), p('it', 4), p('I', 1),
  p('your', 7), p('leave', 3), p('to', 6)
]);
39
52
1

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
39
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?