JavaScript
jQuery
Deferred

Javascriptで作業を自動化するとき役に立つDeferredの使い方

すっかり放置していたけど、やっぱり書いておかないと忘れるよね・・・

事の発端はお仕事で、DBで持ってるリンクの情報を読み込んで、そのページ(外部の)を読み込んで、リンクをポチり、それが終わったら表示されるリンクを押したい(自動で)というお願いをされた。
ポチる条件が、えげつなかったんですがそこは省略。
外部のサイトでAPIがあれば使ったんですが、やりたい事に該当するAPIがない。
ぽちぽちする作業は定期的に発生するのに、人力で毎回2時間とは・・・ヒドイ。
仕方がないので、今回はChrome+Tampermonkeyという組み合わせで回避してみました。

2018/2/4 追記
jQueryなくても素のjavascriptでいけるよっていうコメントを@printf_morikenさんから頂いたので実行結果として追記します

前提条件

  • MacOS High Sierra 10.13.3
  • Chrome 63.0.3239.132
  • Tampermonkey

今回の作業

Javascriptで作業を自動化する

今回のお題としては、それぞれ表示に時間がかかったとしても、
first
second
third
の順に直列処理したい!です。

手順

拡張機能いれる自体は難しくないので、省略

実験

まずは何も考えずノーマルに書いてみる

function callFirst()
{
    console.log('first');
}

function callSecond()
{
    console.log('second');
}

function callThird() {
    console.log('third');
}

console.log('start');
callFirst();
callSecond();
callThird()
console.log('end');

結果

start
first
second
third
end

めちゃシンプルですね。
うまくいっているように見える。が、それはウソですw
ちょっと時間がかかる処理がはいってるという状況をつくるために書き換えて実行します。

function callFirst() {
    setTimeout(function(){
        console.log('first');
    }, 5000);
}

すると

start
second
third
end
first

意図してた順番じゃなくなります。
まぁそりゃそうよねー。
でもsetTimeoutで待たせるのは、なんかちょっと昔っぽいですよねー(昔の人だけどさ)
なんかあるでしょ?こういうの。というわけで今回は使うのがjQuery.Deferredです。

意図した順番になるようにDeferredを使う

jqueryを使えるようにTampermonkeyのヘッダーに

// ==UserScript==
・・・略・・・
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// ==/UserScript==

とjQueryを呼べる用にしておきます。
そして、おもむろにDeferrdを入れます。

function callFirst()
{
    var d = $.Deferred();
    setTimeout(function(){
        console.log('first');
        d.resolve();
    }, 5000);
    return d.promise();
}

function callSecond()
{
    console.log('second');
}

function callThird() {
    console.log('third');
}

console.log('start');
var df = (new $.Deferred()).resolve();
df = df
    .then(callFirst)
    .then(callSecond)
    .then(callThird)
    .then(function(){console.log('end');});

これで

start
first
second
third
end

の順になりました。めでたしめでたし。

thenでつなぐとその順番に処理するぜーって、わりとどのサイトもサラッと書いてあったんですが、はじめてのDeferredな自分には、え?は?みたいな動きをどう自分のやりたい事にマッチさせるか・・・で苦労しました。

ものすごく単純にすると
Deferredを呼ぶ→自分のしたいことの後にresolveを呼ぶ→promiseを返す
promise受け取ってresolveしたら次の処理に移るんだ
という事ですかね(はしょりすぎ)

全体的な動きのちゃんとしたお話については、ちょっと古いですが参考サイトにあげたところが一番しっくりきたので、貼っておきます。

それとcallFirstに引数渡したい!な時とか、ん?あれ?ってなったんですが

.then(function() { return callFirst(hoge);})

みたいにすると、ちゃんと動いてました(これが正しいとは言い切れない・・・)

参考サイト

爆速でわかるjQuery.Deferred超入門

jQueryなしで意図した順番にする(2018/2/4 追記)

コメント欄で教えて頂いた、jQuery使わんでもいけるよっていう方法を実行結果とともに。

function callFirst() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log('first');
            resolve();
        }, 5000);
    });
}

function callSecond() {
    console.log('second');
}

function callThird() {
    console.log('third');
}

console.log('start');
Promise.resolve()
    .then(callFirst)
    .then(callSecond)
    .then(callThird)
    .then(function() {
        console.log('end');
    });

を実行すると

start
first
second
third
end

おお。素敵。
さらに、引数渡す版

function callFirst(arg) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log('first', arg);
            resolve(arg + 1); // 次の函数に arg + 1 をわたす
        }, 5000);
    });
}

function callSecond(arg) {
    console.log('second', arg);
    return arg + 1; // 次の函数に arg + 1 をわたす
}

function callThird(arg) {
    console.log('third', arg);
    return arg + 1; // 次の函数に arg + 1 をわたす
}

console.log('start');
Promise.resolve(1) // callFirst に 1 を渡す
    .then(callFirst)
    .then(callSecond)
    .then(callThird)
    .then(function(arg) {
        console.log('end', arg);
    });

を実行すると

start
first 1
second 2
third 3
end 4

おお。謎が解けました。 @printf_moriken さん、ありがとうございました。

最近、フロント周りは、前から存在するコードの修正しかしてないから、jQueryにあるんじゃね?くらいしか思い浮かばなかったので、もうちょっと広げて探さないとダメですね。