Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Promise と async/await
Search
Jeongmin LEE
October 16, 2024
0
14
Promise と async/await
Jeongmin LEE
October 16, 2024
Tweet
Share
More Decks by Jeongmin LEE
See All by Jeongmin LEE
シングルな Javascript の非同期処理
gardensky511
0
83
集合で理解する Typescript
gardensky511
1
150
Javascript のデータ型 プリミティブ型・オブジェクト
gardensky511
0
110
Featured
See All Featured
GraphQLの誤解/rethinking-graphql
sonatard
67
10k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
29
2.3k
Into the Great Unknown - MozCon
thekraken
32
1.5k
A Philosophy of Restraint
colly
203
16k
Producing Creativity
orderedlist
PRO
341
39k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.1k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
44
2.2k
Java REST API Framework Comparison - PWX 2021
mraible
PRO
28
8.2k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
6
410
10 Git Anti Patterns You Should be Aware of
lemiorhan
654
59k
Ruby is Unlike a Banana
tanoku
97
11k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
28
2k
Transcript
Promise と async/await Javascript 非同期処理完全理解(2)
アイスブレイクでまずみなさんに質問をします
この2つのコードで型が違う理由ご存知ですか? (知ってる人はネタバレ禁止です笑)
わからなくても大丈夫
この勉強会で完全理解できる!
職業 自己紹介 韓国人です。大学時代にはげしくアニメ オタクをやってて日本語が上手くなりま した。 フロントエンドエンジニア(2020/11 ~) React、Typescript メインでやってます
CONTENTS 1. Callback 関数の限界 2. Promise 3. async/await 4. まとめ
1. Callback 関数の限界
let g = 0; setTimeout(() => { g = 100
}, 0) console.log(g) Q. console.log には何が出力されるでしょうか?
console.log は setTimeout の内部処理がわからない let g = 0; setTimeout(() =>
{ g = 100 }, 0) console.log(g) Q. console.log には何が出力されるでしょうか? 正解:0
Memory Heap Call Stack Web APIs Task Queue Event Loop
let g = 0; (されて CallStack から消える) ①実行 let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) ①実行 なぜわからないのか
Memory Heap Call Stack Web APIs Task Queue Event Loop
setTimeout(() => { g = 100 }, 0) let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか ②実行 ②実行
Memory Heap Call Stack Web APIs Task Queue Event Loop
setTimeout(() => { g = 100 }, 0) let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか ②実行 setTimeout(() => { g = 100 }, 0) こっちに移動する
Memory Heap Call Stack Web APIs Task Queue Event Loop
let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか ②実行 setTimeout(() => { g = 100 }, 0) setTimeout が実行されたとみなされ、 Call Stack からは消える
Memory Heap Call Stack Web APIs Task Queue Event Loop
let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか () => { g = 100 } コールバック関数を Task Queue に送る
Memory Heap Call Stack Web APIs Task Queue Event Loop
let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか console.log(g) ③実行 (この時点の g の値は 0) ③実行 () => { g = 100 } (されて CallStack から消える)
Memory Heap Call Stack Web APIs Task Queue Event Loop
let g = 0; setTimeout(() => { g = 100 }, 0) console.log(g) なぜわからないのか () => { g = 100 } ④実行 ④実行 非同期処理のコールバック関数は 同期処理が終わった後に実行される
非同期処理結果に対する後続処理は、コールバック関数で行う必要がある let g = 0; setTimeout(() => { g =
100; console.log(g); // 100 }, 0)
でもコールバック関数だけでは限界があり、、
const get = (url, callback) => { const xhr =
new XMLHttpRequest(); xhr.open('GET', url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) callback(JSON.parse(xhr.response)); else console.error(xhr.statusText); }; }; 1. Callback Hell による可読性の低下 データを取得する非同期関数を作ってみよう
get('なにかのurl', (a) => { console.log(a) }) 使うときは url と callback
関数を渡す
ネストし始めたら地獄 (Callback Hell) a の結果で b を GET して、その結果でまたcをGETして、その結果でまた(以下略) get(‘/step1’,
(a) => { get(`/step2/${a}`, (b) => { get(`/step3/${b}`, (c) => { get(`/step4/${c}`, (d) => { console.log(c) }) }) }) })
Image From wikipedia https://www.linkedin.com/pulse/callback-hell-javascript-abir-tasrif-anto/ 可読性が落ちてミスが多発する
2. エラー処理が難しい try { throw new Error("Error!"); } catch (error)
{ console.error("エラー", error) } try-catch エラーを発生させてみよう
try { throw new Error("Error!"); } catch (error) { console.error("エラー",
error) } try-catch エラーを発生させてみよう エラーをキャッチしてる 2. エラー処理が難しい
try { setTimeout(() => { throw new Error("Error!") }, 0)
} catch (error) { console.error("エラー", error) } try-catch エラーを発生させてみよう 2. エラー処理が難しい
try { setTimeout(() => { throw new Error("Error!") }, 0)
} catch (error) { console.error("エラー", error) } try-catch エラーを発生させてみよう エラーがキャッチできない 2. エラー処理が難しい
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか try-catch ブロックが コールスタックに入る
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか setTimeout(() => { throw new Error("Error!") }, 0) setTimeout は こっちに移動する
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { } catch (error) { console.error("エラー", error) } try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか setTimeout(() => { throw new Error("Error!") }, 0) setTimeout が実行されたとみなされ、 Call Stack からは消える
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { } catch (error) { console.error("エラー", error) } try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか () => { throw new Error("Error!") } コールバック関数を Task Queue に移動、 コールスタックが空になるのを待つ
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { } catch (error) { console.error("エラー", error) } try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか 同期処理である try–catch ブロックの 残り処理が実行される。この時点では エラーがないので、正常終了する () => { throw new Error("Error!") }
Memory Heap Call Stack Web APIs Task Queue Event Loop
try { setTimeout(() => { throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } なぜエラーがキャッチできないのか キューで待ってたコールバック関数が エラーを発生してるが、もう try-catch ブロックは終了した後なの でエラーキャッチができない () => { throw new Error("Error!") }
この限界を克服するために登場したものが、、
3. Promise
Promise とは - ES6(2015年)で投入された、非同期処理の状態や結果を表現するビルトインオブジェクト - 非同期作業をより簡単に処理できる
Promise とは - ES6(2015年)で投入された、非同期処理の状態や結果を表現するビルトインオブジェクト - 非同期作業をより簡単に処理できる
非同期処理の状態 pending fulfilled rejected 非同期処理がまだ行われてない状態 非同期処理が完了した状態(成功) 非同期処理が完了した状態(失敗) 非同期処理には3つの状態が存在する
非同期処理の状態 pending fulfilled rejected 非同期処理がまだ行われてない状態 非同期処理が完了した状態(成功) 非同期処理が完了した状態(失敗) 非同期処理には3つの状態が存在する settled (pending
ではない状態)
補足) settled になったら状態は変化しない const promise = new Promise((resolve, reject) =>
{ setTimeout(() => { resolve(); // すでにresolveされているため無視される reject(new Error("エラー")); }, 16); }); promise.then(() => { console.log("Fulfilledとなった"); }, (error) => { // この行は呼び出されない }); 一度でも Settled となった Promise インスタンスは、それ以降別の状態には変化しない
補足) settled になったら状態は変化しない const promise = new Promise((resolve, reject) =>
{ setTimeout(() => { resolve(); // すでにresolveされているため無視される reject(new Error("エラー")); }, 16); }); promise.then(() => { console.log("Fulfilledとなった"); }, (error) => { // この行は呼び出されない }); 一度でも Settled となった Promise インスタンスは、それ以降別の状態には変化しない 実際実行してみた
Promise インスタンス - Promise はクラスなので new 演算子で Promise のインスタンスを作成し、利用する -
コンストラクタには resolve 関数と reject 関数の2つの引数を取る関数(executor)を渡す const promise = new Promise((resolve, reject) => { // 非同期の処理が成功したときはresolveを呼ぶ // 非同期の処理が失敗したときはrejectを呼ぶ });
Promise インスタンスには状態変化をした際に呼び出されるコールバック関数を登録できる Promise で非同期処理を扱う
Promise インスタンスには状態変化をした際に呼び出されるコールバック関数を登録できる fulfilled or rejected の2択 (Promise 生成直後の基本状態は pending のため)
= 非同期処理が成功したときと、失敗したときのコールバック関数が登録できる Promise で非同期処理を扱う
const promise = () => { return new Promise((resolve, reject)
=> { const success = Math.random() > 0.5; // 50%の確率で成功 setTimeout(() => { if (success) resolve("成功!"); // 非同期処理成功 else reject("失敗!"); // 非同期処理失敗 }, 2000); }); } 非同期処理をする Promise インスタンスを返す関数を作成してみよう
promise() .then((result) => { console.log(result); // 非同期処理成功 }).catch((error) => {
console.error(error); // 非同期処理失敗 }); 非同期処理の結果を扱う then や catch メソッドで成功時や失敗時に呼び出される処理をコールバック関数として登録する
status pending result undefined status “fulfilled” result value status “rejected”
result error resolve(value) reject(error) Promise の状態と結果値
get(‘/step1’, (a) => { get(`/step2/${a}`, (b) => { get(`/step3/${b}`, (c)
=> { get(`/step4/${c}`, (d) => { console.log(c) }) }) }) }) Promise のメリット(1) - Callback Hell の解消 get('/step1') .then((a) => get(`/step2/${a}`)) .then((b) => get(`/step3/${b}`)) .then((c) => get(`/step4/${c}`)) .then((d) => { console.log(d); }) Callback Promise
get(‘/step1’, (a) => { get(`/step2/${a}`, (b) => { get(`/step3/${b}`, (c)
=> { get(`/step4/${c}`, (d) => { console.log(c) }) }) }) }) Promise のメリット(1) - Callback Hell の解消 Callback Promise get('/step1') .then((a) => get(`/step2/${a}`)) .then((b) => get(`/step3/${b}`)) .then((c) => get(`/step4/${c}`)) .then((d) => { console.log(d); }) Promise の後続処理メソッドはいつも Promise インスタンスを返すので、連続的に 呼び出すことができる。これをPromise チェーン (promise chaining)と呼ぶ。
Promise のメリット(2) - エラーハンドリング Callback Promise try { setTimeout(() =>
{ throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } new Promise((_, reject) => { setTimeout(() => { reject(new Error("Error!")) }, 0) }).catch(error => { console.error("エラー", error) })
Promise のメリット(2) - エラーハンドリング Callback Promise try { setTimeout(() =>
{ throw new Error("Error!") }, 0) } catch (error) { console.error("エラー", error) } new Promise((_, reject) => { setTimeout(() => { reject(new Error("Error!")) }, 0) }).catch(error => { console.error("エラー", error) }) エラーがキャッチできる
でも Promise にも限界はある
Promise の限界(1) - あいかわらず複雑 get('/step1') .then((a) => get(`/step2/${a}`)) .then((b) =>
get(`/step3/${b}`)) .then((c) => get(`/step4/${c}`)) .then((d) => { console.log(d); }) Callback Hell よりは改善されたけど、 非同期処理が複数あって Promise チェーンが 長くなったらやはり可読性が落ちる
Promise の限界(2) - エラーハンドリングも複雑になりえる get('/step1') .then((a) => get(`/step2/${a}`)) .then((b) =>
get(`/step3/${b}`)) .then((c) => get(`/step4/${c}`)) .then((d) => { console.log(d); }) このコードでエンドポイントごとにエラーハンドリングをしたら?
Promise の限界(2) - エラーハンドリングも複雑になりえる get('/step1') .then(a => get(`/step2/${a}`)) .catch(error =>
console.error(`Step 1 failed: ${error}`)) .then(b => get(`/step3/${b}`)) .catch(error => console.error(`Step 2 failed: ${error}`)) .then(c => get(`/step4/${c}`)) .catch(error => console.error(`Step 3 failed: ${error}`)) .then(d => console.log(d)) .catch(error => console.error(`Step 4 failed: ${error}`)); (なにがなんだか、、)
この限界を克服するために登場したものが、、
3. async/await
async/await とは - ES8(2017年)で投入された、非同期処理を行う関数を定義する構文 - Async Function は通常の関数とは異なり、必ずPromiseインスタンスを返す
async function doAsync() { return "値"; } doAsync().then(value => {
console.log(value); // => "値" }); Async Function は必ずPromiseインスタンスを返す Async Function は関数の定義に async キーワードをつけることで定義できる Async Function *Typescript の型推論でも Promise 型になる
async function doAsync() { return "値"; } doAsync().then(value => {
console.log(value); // => "値" }); Async Function は必ずPromiseインスタンスを返す function doAsync() { return Promise.resolve("値"); } doAsync().then(value => { console.log(value); // => "値" }); Async Function 通常の Function Async Functionは Promise の上に作られた構文 *返り値をラップしたPromiseインスタンスを返す この2つのコードは同じことをやってる
const promise = () => { return new Promise((resolve, reject)
=> { const success = Math.random() > 0.5; // 50%の確率で成功 setTimeout(() => { if (success) resolve("成功!"); // 非同期処理成功 else reject("失敗!"); // 非同期処理失敗 }, 2000); }); } Async Function で非同期処理を扱ってみよう 非同期処理をするために Promise インスタンスを返す関数を作成(ここまでは先と同じ)
promise() .then((result) => { console.log(result); // 非同期処理成功 }).catch((error) => {
console.error(error); // 非同期処理失敗 }); const handlePromise = async () => { try { const result = await promise(); console.log(result); // 非同期処理成功 } catch (error) { console.error(error); // 非同期処理失敗 } } 非同期処理の結果を扱う Promiseチェーン Async Function
promise() .then((result) => { console.log(result); // 非同期処理成功 }).catch((error) => {
console.error(error); // 非同期処理失敗 }); const handlePromise = async () => { try { const result = await promise(); console.log(result); // 非同期処理成功 } catch (error) { console.error(error); // 非同期処理失敗 } } 非同期処理の結果を扱う Promiseチェーン Async Function Async Function内では await 式が利用できる
await 式 - Promise の非同期処理が完了するまで待つ構文 - Promiseインスタンスの状態が変わると、次の行の処理を再開 async function doAsync()
{ // 非同期処理 } async function asyncMain() { // doAsyncの非同期処理が完了するまでまつ await doAsync(); // 次の行はdoAsyncの非同期処理が完了されるまで実行されない console.log("この行は非同期処理が完了後に実行される"); }
await 式 - Promise の非同期処理が完了するまで待つ構文 - Promiseインスタンスの状態が変わると、次の行の処理を再開 async function doAsync()
{ // 非同期処理 } async function asyncMain() { // doAsyncの非同期処理が完了するまでまつ await doAsync(); // 次の行はdoAsyncの非同期処理が完了されるまで実行されない console.log("この行は非同期処理が完了後に実行される"); } - fulfilled の場合、resolve された値が await 式の返り値となる - rejected の場合、その場でエラーを throw する
最初の質問の正解大公開 await で非同期処理の完了を待つので、resolve され た値である string が推論される await がない=非同期処理の完了を待たないので、 Promise<string>
型が推論される
get('/step1') .then((a) => get(`/step2/${a}`)) .then((b) => get(`/step3/${b}`)) .then((c) => get(`/step4/${c}`))
.then((d) => { console.log(d); }) Promise async function fetchData() { const a = await get('/step1'); const b = await get(`/step2/${a}`); const c = await get(`/step3/${b}`); const d = await get(`/step4/${c}`); } fetchData(); async/await のメリット(1) - 非同期を同期のように Async Function
get('/step1') .then(a => get(`/step2/${a}`)) .catch(error => console.error(`Step 1 failed: ${error}`))
.then(b => get(`/step3/${b}`)) .catch(error => console.error(`Step 2 failed: ${error}`)) .then(c => get(`/step4/${c}`)) .catch(error => console.error(`Step 3 failed: ${error}`)) .then(d => console.log(d)) .catch(error => console.error(`Step 4 failed: ${error}`)); async/await のメリット(2) - エラーハンドリングが分かりやすく 先のなにがなんだかコード
async function fetchData() { const a = await fetchStep1(); if
(!a) return; // エラーが起きたら終了 const b = await fetchStep2(); if (!b) return; // エラーが起きたら終了 const c = await fetchStep3(); if (!c) return; // エラーが起きたら終了 const d = await fetchStep4(); if (d) console.log(d); } fetchData(); async function fetchStep1() { try { return await get('/step1'); } catch (error) { console.error(`Step 1 failed: ${error}`); return null; // エラー発生時は null 返す } } async/await のメリット(2) - エラーハンドリングが分かりやすく こんな感じで各ステップを関数で切り出して、、 順番通り呼び出す
4. まとめ
コールバック関数 - Callback Hell、エラーキャッチが難しいという限界がある Promise - ES6(2015年)で投入された、非同期処理の状態や結果を表現するビルトインオブジェクト - Promise チェーンを使って
Callback Hell を解決できるし、エラーもキャッチできる - Promise チェーンが長くなったら複雑になるし、部分的なエラーハンドリングも複雑になりえる async/await - EES8(2017年)で投入された、非同期処理を行う関数を定義する構文 - Async FunctionはPromiseの上に作られた構文であり、必ず Promise インスタンスを返す - 非同期処理を同期処理のように使えるし、エラーハンドリングも分かりやすくなる
ご清聴ありがとうございました
- 이웅모, 『모던 자바스크립트 Deep Dive : 자바스크립트의 기본 개념과
동작 원리』, 위키북스 , 2020 - JavaScript Primer, 「[ES2015] Promise」, https://jsprimer.net/basic/async/#promise, (2024.10.14) - JavaScript Primer, 「[ES2017] Async Function」,https://jsprimer.net/basic/async/#async-function , (2024.10.15) 参考資料