0
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 3 years have passed since last update.

ボヤッと使っていたPromiseをしっかり理解する①

Posted at

最初に

複数回に分けてJavaScript Promiseの本を読んでいきまとめていきます。
*自己解釈して追記したりしている部分があります。

1.1 Promiseとは何か?

Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みのこと。
JavaScriptにおける非同期処理といえば、コールバックを利用する場合が多いが、ルールが統一されていないため、書き方がいろいろある。
そこで、統一的なインターフェースで書くように非同期に対するオブジェクトとルールを仕様化したのがPromise。

//コールバックを使った非同期処理の一例
getAsync("fileA.txt", (error, result) => { 
    if (error) { // 取得失敗時の処理
        throw error;
    }
    // 取得成功の処理
});

//Promiseを使った非同期処理の一例
const promise = getAsyncPromise("fileA.txt"); 
promise.then((result) => {
    // 取得成功の処理
}).catch((error) => {
    // 取得失敗時の処理
});

非同期処理を抽象化したpromiseオブジェクトというものを用意し、 そのpromiseオブジェクトに対して成功時の処理と失敗時の処理の関数を登録するようにして使う。
コールバック関数と比べると何が違うのかは、 非同期処理の書き方がpromiseオブジェクトのインターフェースに沿った書き方に限定されている点。
つまり、promiseオブジェクトに用意されているメソッド(ここでは then や catch)以外は使えないため、 コールバックのように引数に何を入れるかが自由に決められるわけではなく、一定のやり方に統一される。

Promiseの役割まとめ

  • 非同期処理において統一されたインターフェースを提供し、非同期処理をパターン化しやすくする

1.2 Promiseの概要

PromiseのAPIは大きく分けて3つ

  • Constructor

    • コンストラクタ関数である Promise からインスタンスとなる Promise オブジェクトを作成する。
    const promise = new Promise((resolve, reject) => {
        // 非同期の処理
        // 処理が終わったら、resolve または rejectを呼ぶ
    });
    
  • Instance Method

    • .then().catch() などのこと
    • resolve(成功) / reject(失敗) した時に呼ばれる コールバック関数を登録する役割がある
  • Static Method

    • Promise.all()Promise.resolve() などPromiseを扱う上での補助メソッドなど
// Promiseの書き方例
function asyncFunction() {
    
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Async Hello world");
        }, 16);
    });
}

asyncFunction().then((value) => {
    console.log(value); // => 'Async Hello world'
}).catch((error) => {
    console.error(error);
});

catchを使わずに書く場合

asyncFunction().then((value) => {
    console.log(value);
}, (error) => {
    console.error(error);
});

Promiseの状態

new Promise でインスタンス化したpromiseオブジェクトには3つの状態がある。

  • Fulfilled

    • 成功(resolve)した状態:この状態になった時, onFulfilled が呼ばれる。
  • Rejected

    • 失敗(reject)した状態:この状態になった時, onRejected が呼ばれる。
  • Pending

    • 上記2つ以外の場合→オブジェクト作成から他の状態にうつるまではこの状態ということ

ここからわかることは、 pendding から他の状態になった後、Promiseオブジェクトの状態はそれ以上変化しないということ。
つまり、 .then() などで登録したコールバック関数が呼ばれるのは1度だけ
このようなことから、FulfilledとRejectedのどちらかの状態であることをSettled(不変の)と表現することがある。


1.3 Promiseの書き方

Promiseオブジェクトの作成

作成の流れは以下のよう

  1. new Promise(function) でPromiseオブジェクトをインスタンス化
  2. functionでは非同期などの処理をかく
  • 処理結果が正常ならresolve(結果の値) を呼ぶ
  • 処理結果がエラーなら、reject(Errorオブジェクト) を呼ぶ
// 作成の例
function fetchURL(URL) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = () => {
            if (200 <= req.status && req.status < 300) {
                resolve(req.responseText);//①
            } else {
                reject(new Error(req.statusText));//②
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}

// 実行例
const URL = "https://httpbin.org/get";
fetchURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

①の resolve(req.responseText) ではレスポンスの内容を引数に入れている。

こうすることで、コールバックと同様に値を次の処理(thenなど)へ渡せる。

②のようなエラーの場合、reject(new Error(req.statusText))のようにエラーの内容を引数に入れている。

こうすることで、コールバックと同様に値を次の処理(catchなど)へ渡せる。

reject に渡す値に制限はありませんが、一般的にErrorオブジェクト(またはErrorオブジェクトを継承したもの)を渡すことになっている。

Promise.resolve

Promise.resolve(value) という静的メソッドは、 new Promise() のショートカットとなるメソッド。
例)
Promise.resolve(42); は以下のコードのシンタックスシュガー

new Promise((resolve) => {
  resolve(42);
});

resolveメソッドで作成したPromiseオブジェクトも .then を使った処理を書くことができる

Promise.resolve(42).then((value) => {
  console.log(value);
 });

Promise.resolve の大きな特徴として、thenableなオブジェクトをPromiseオブジェクトに変換するという機能がある。

thenableとは、Promiseっぽいオブジェクトのことをいう

通常の使用ではあんまり使う機会がないので、こういった機能があるんだということだけ覚えとけば良さそう。

まとめ

Promise.resolve は、「渡した値でFulfilledされるPromiseオブジェクトを返すメソッド」という感じ

Promise.reject

Promise.reject(error)Promise.resolve(value) と同じ静的メソッドで、 new Promise() のショートカットとなるメソッド。
例)
Promise.reject(new Error("エラー")) は以下のコードのシンタックスシュガー

new Promise((resolve, reject) => {
  reject(new Error("エラー"));
});

以下のようにcatchを使うことができる

Promise.reject(new Error("エラー")).catch((error) => {
  console.log(error);
});

まとめ

Promise.reject は、「渡した値でRejectedされるPromiseオブジェクトを返すメソッド」という感じ

Promise.resolvePromise.reject はPromiseオブジェクトがすぐ、 resolve/reject されるので、 then/catch に登録された関数もすぐに処理されるので、同期的かと錯覚してしまうけど、実際は非同期。

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

// 実行結果
inner promise // 1
outer promise // 2
42            // 3

なぜ同期的に実行できるのに非同期で実行するのか?

配置する場所によって、 コンソールに出てくるメッセージの順番が変わってしまうコードがあるため。
つまり配置する場所によって結果が変わってしまうため、あえて非同期でしている。
この問題は、 「Effective JavaScript」 の 「項目67 非同期コールバックを同期的に呼び出してはいけない 」で以下のように記載されている

  • 非同期コールバックは(たとえデータが即座に利用できても)決して同期的に使ってはならない。
  • 非同期コールバックを同期的に呼び出すと、処理の期待されたシーケンスが乱され、 コードの実行順序に予期しない変動が生じるかもしれない。
  • 非同期コールバックを同期的に呼び出すと、スタックオーバーフローや例外処理の間違いが発生するかもしれない。
  • 非同期コールバックを次回に実行されるようスケジューリングするには、setTimeout のような非同期APIを使う。

以上のことから、 Promiseは常に非同期で処理されるということが仕様で定められている。

promise#then

Promiseではいくらでもメソッドチェーンをつなげて処理をかける。
つぎのようなメソッドチェーン

function taskA() {
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

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

promise chainでの値渡し

メソッドチェーンで値渡しする場合は前のチェーンでreturnすればOK

function doubleUp(value) {
    return value * 2;
}
function increment(value) {
    return value + 1;
}
function output(value) {
    console.log(value);// => (1 + 1) * 2
}

const promise = Promise.resolve(1);
promise
    .then(increment)
    .then(doubleUp)
    .then(output)
    .catch((error) => {
        // promise chain中にエラーが発生した場合に呼ばれる
        console.error(error);
    });

このreturnする値は、数値や文字列だけではなく、オブジェクトやPromiseオブジェクトもReturnできる。
Returnした値は、 Promise.resolve(returnされた値)のように処理されるので、なにをReturnしても最終的には新しいPromiseオブジェクトが返される。

まとめ

Promise.thenは、単にコールバック関数をとうろくするだけではなく、受け取った値いを変換させてべつのPromiseオブジェクトを生成するという機能も持っている。

promise#catch

Promise.catch は promise.then(undefined, onrejected) のエイリアスとなるメソッド。

IE8以下はECMAScript 3の実装なので、catchが使えない(予約語)

promise#finally

ECMASScript 2018からPromise Chainの最後に処理を実行する Promise#finally メソッドが追加された。

Promise#finally メソッドは成功失敗にかかわらず呼び出すコールバック関数を登録する。

try...catch...finally と同様の役割を持つメソッド。

Promise.resolve("成功").finally(() => {
    console.log("成功時に実行される");
});
Promise.reject(new Error("失敗")).finally(() => {
    console.log("失敗時に実行される");
});

finally メソッドのコールバック関数は引数を受け取らず、どのような値いをreturnしてもPromise Chainには影響を与えない。

また、finallyメソッドは新しいPromiseオブジェクトを返し、新しいPromiseおぶじぇくとは呼び出し元のPromiseオブジェクトをの内容をそのまま返す。

function onFinally() {
    // 成功、失敗どちらでも実行したい処理
}

// `Promise#finally` は新しいpromiseオブジェクトを返す
Promise.resolve(42)
    .finally(onFinally)
    .then((value) => {
        // 呼び出し元のpromiseオブジェクトの状態をそのまま引き継ぐ
        // 呼び出し元のpromiseオブジェクトは `42` で resolveされている
        console.log(value); // 42
    });

これの使い時は、APIでデータを取得中のフラグを成功失敗にかかわらず、終わったらfalseにする場合など。

Promiseと配列

Promise.all というものがある。
とりあえずthenで複数の非同期処理を実行する例を以下に示す。

function fetchURL(URL) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = () => {
            if (200 <= req.status && req.status < 300) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
const request = {
    comment() {
        return fetchURL("https://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
    },
    people() {
        return fetchURL("https://azu.github.io/promises-book/json/people.json").then(JSON.parse);
    }
};
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] は記録する初期値を部分適用している
    const pushValue = recordValue.bind(null, []);
    return request.comment()
        .then(pushValue)
        .then(request.people)
        .then(pushValue);
}



// 実行例
main().then((value) => {
    console.log(value);
}).catch((error) => {
    console.error(error);
});

上記のように結構くどい。
このような複数の非同期処理をまとめてすっきり扱う、 Promis.allPromise.raceという静的メソッドがある

Promise.all

Promise.allはPromiseオブジェクトの配列を受け取り、その配列に入っているPromiseオブジェクトがすべてresolveされたときに、つぎの .thenを呼び出す。

先ほどの処理をPromise.allで書くと以下のようになる。

function fetchURL(URL) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = () => {
            if (200 <= req.status && req.status < 300) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
const request = {
    comment() {
        return fetchURL("https://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
    },
    people() {
        return fetchURL("https://azu.github.io/promises-book/json/people.json").then(JSON.parse);
    }
};
function main() {
    return Promise.all([request.comment(), request.people()]);
}



// 実行例
main().then((value) => {
    console.log(value);
}).catch((error) => {
    console.error(error);
});

Promise.all([request.comment(), request.people()])でcomment()とpeopleは同時に実行されるが、それぞれの結果はallに渡した配列の順番で保存される。

このことは以下のコードを実行するとわかる。

// `delay`ミリ秒後にresolveする
function timerPromisefy(delay) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
const startDate = Date.now();
// 全てがresolveされたら終了
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then((values) => {
    console.log(Date.now() - startDate + "ms");// 約128ms
    console.log(values); // [1,32,64,128]
});

結果

[object Promise]
129ms
[ 1, 32, 64, 128 ]

Promise.race

Promise.all と同様に複数のpromiseオブジェクトを扱うもの。

使い方は.allと一緒だが、並列実行の非同期処理のうちどれか一つが終わればつぎの処理を実行する。

// `delay`ミリ秒後にresolveする
function timerPromisefy(delay) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
// 一つでもresolve または reject した時点で終了
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then((value) => {
    console.log(value); // => 1
});

結果

[object Promise]
1

各非同期処理自体は実行されるが、次の処理に値が引き渡されるのはresolveして戻ってきた最初の値のみ。

then or catch?

then(undefined, onRejected)catchはどっちをつかうのがいいのか?

以下のような例の1でthenを使ってエラーハンドリングしようとすると、正しくハンドリングできないので、catchを使おう

function throwError(value) { // 例外を投げる
    throw new Error(value);
}
// <1> onRejectedが呼ばれることはない
function badMain(onRejected) {
    return Promise.resolve(42).then(throwError, onRejected);
}
// <2> onRejectedが例外発生時に呼ばれる
function goodMain(onRejected) {
    return Promise.resolve(42).then(throwError).catch(onRejected);
}



// 実行例
badMain(function(){
    console.log("BAD");
});
goodMain(function(){
    console.log("GOOD");
});

まとめ

  1. promise.then(onFulfilled, onRejected) において
  • onFulfilled で例外がおきても、この onRejected はキャッチできない
  1. promise.then(onFulfilled).catch(onRejected) とした場合
  • then で発生した例外を .catch でキャッチできる
  1. .thenと.catchに本質的な意味の違いはない
  • 使い分けると意図が明確になる

参考

0
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
0
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?