JavaScript の非同期周りの話

ピクシブ株式会社 アルバイト
GFTD. FUKUOKA WORKS 職業指導員
森建 (https://social.kimamass.com/@moriken)

どんな人?

ECMAScript と DOM API が好きな人

ES Discuss で提案を投げまくってる
Float16Array (パッケージ紹介, ESNext Stage 1) - Qiita

9/22 FUKUOKA Engineers Day 2018 ~Autumn~ で Cloud Functions で OGP の画像を自動生成する話をする予定です

ES2015 の復習

ES2015 Promise

  • callback 地獄からの開放
  • 並行処理に強い
    
    Promise.all([promise1, promise2]);
                                
  • 直列処理に弱い
    
    promise
        .then((result1) => task1(result1))
        .then((result2) => task2(result2));
                                
    result1 を task2 で使いたいとか考え出すと更に面倒なことになる

ES2015 Generator Functions

処理を途中で中断できる函数


// 処理宣言部分
function* generator(initValue) {
    const nextValue = yield initValue;
    yield nextValue + initValue;
}

// 処理実行部分
const iterator = generator(9);
/** @type {{value: any, done: boolean}} */
let iteratorResult;
do {
    iteratorResult = iterator.next(3);
    console.log(iteratorResult.value); // 9, 12
} while(iteratorResult.done);
                    

co, そして ES2017 Async Functions へ……

Promise, Generator Functions を組み合わせて直列処理も書きやすくしよう!


// 処理宣言部分
function* createTask() {
    const result1 = yield asyncTask(); // Promise を返す函数
    yield asyncTask2(result1);
    yield asyncTask3(result1, result2);
}

// 処理実行部分
(function exec(task, result) {
    // 前の結果を yield に渡す
    const iteratorResult = task.next(result);

    // イテレータが完了したら終わる
    if (iteratorResult.done) { return; }

    // Promise が fullfilled されたら再帰する
    iteratorResult.value.then((result) => exec(task, result));
})(createTask());
                    

co


import co from "co";

// 処理宣言部分
const createTask = co.wrap(function* () {
    const result1 = yield asyncTask(); // Promise を返す函数
    const result2 = yield asyncTask2(result1);
    yield asyncTask3(result1, result2);
});

// 処理実行部分
createTask();
                    

Generator Functions の処理中断(yield)を利用して、Promise を yield したら fullfilled されるまで待って再度途中から実行する

ES2017 Async Functions


// 処理宣言部分
async function createTask() {
    const result1 = await asyncTask(); // Promise を返す函数
    const result2 = await asyncTask2(result1);
    await asyncTask3(result1, result2);
});

// 処理実行部分
createTask();
                    

yield の後ろに Promise が来たときに fullfilled まで待つのをシンタックスでサポート

redux-saga, そして ES2018 Async Generator Functions へ……

Flux

Flux Diagram
Flux: Actions and the Dispatcher - React Blog

Flux で非同期処理といえば Action Creators だが、Async Functions だと複数の非同期タスクの進捗度の変化がわからない


async function createAction(obj) {
    const result1 = await asyncTask(); // 非同期タスクの実行
    const result2 = await asyncTask2(result1);
    obj.value = result2; // ただ value に入れているだけ(普通は変更検知不可能)
    await asyncTask3(result1, result2);
    obj.value = result3;
});
                    

結果としてテストしづらい

Object.observe() - JavaScript | MDN

redux-saga


import * as Effect from "redux-saga/effects";

function* createAction() {
    const result1 = yield Effect.call("AsyncTask1"); // 非同期タスクの実行
    const result2 = yield Effect.call("AsyncTask2", { result1 });
    yield Effect.put(result2); // Store に入れる
    const result3 = yield Effect.call("AsyncTask3", { result1, result2 });
    yield Effect.put(result3);
});
                    

yield の後ろを Effect で場合分けしている

co がやってたのと同じように実行して、Effect.put のときだけ検知すれば進捗度がわかる

ES2018 Async Generator Functions


async function* createAction() {
    const result1 = await asyncTask(); // 非同期タスクの実行
    const result2 = await asyncTask2(result1);
    yield result2;
    const result3 = await asyncTask3(result1, result2);
    yield result3;
});
                    

将来的に Action Creators はこうなるかもしれない🤔

ES2018 Async Iteration - Qiita

おまけ

Stage 1 Observable (RxJS) と Async Functions で一応同じようなことが出来る


async function createAction(subject) {
    const result1 = await asyncTask(); // Promise を返す函数
    const result2 = await asyncTask2(result1);
    subject.next(result2);
    const result3 = await asyncTask3(result1, result2);
    subject.next(result3);
});
                

……がこの場合はステップごとのテストがやりにくい(Push-based Streams は途中で止められない)
Redux-Observable Epic vs Redux-Saga: 何が問題なのか