okinawa

IT勉強メモ

【JavaScript】非同期処理の基本

非同期処理のイメージ

処理スタート

→非同期処理もスタート、でも完了は後回し

→先に同期処理が完了

→非同期処理が終わった順に完了

必ず、同期処理が先に完了する!

非同期処理の大前提!

非同期処理を実装するには、非同期的な処理をする関数を使うこと。

例えば

  • setTimeout()
  • fs.readFile()
  • Promise()
  • async function()
  • XMLHttpRequest.onreadystatechange

などのこと。

ネットで調べるとコールバック関数=非同期みたいに見えてしまうが、全然そんなことはないので要注意。

誤解を恐れずに言えば、コールバック関数・Promise・async/awaitは非同期処理を同期処理にする時に使う。

非同期でバラバラに完了すると困るから同期処理にしちゃいましょうねーという感じ。

・参考

JavaScript中級者への道【4. 非同期関数】

https://qiita.com/matsuby/items/3dd9cfca8c7b0685e474

JavaScript中級者への道【5. コールバック関数】

https://qiita.com/matsuby/items/3f635943f25e520b7c20

JavaScriptの非同期処理を理解する その1 〜コールバック編〜

https://knowledge.sakura.ad.jp/24888/

非同期処理の書き方4つ

とりあえず書き方だけ。それぞれの詳しいことは後述。

・普通に書く これ非同期処理なんです。

なぜならsetTimeout関数が非同期で動く関数だから。

同時に3つのsetTimeoutが動き始める。

function before(string) {
  console.log(string);
}

function sample() {
  setTimeout(before, 3000, 1);
  setTimeout(before, 2000, 2);
  setTimeout(before, 1000, 3);
}
sample();

・コールバック関数

setTimeout(function one() {
  console.log(2000)
  setTimeout(function two(){
    console.log(4000)
    setTimeout(function three() {
      console.log(6000);
    }, 6000);
  },4000);
},2000);

・Promise

function async(param){
  return new Promise(function(resolve, reject) {
  setTimeout(function(){
    resolve(param);
  }, 2000);
});
}

async(2000)
  .then(function(param){
    console.log(param);
    return async(4000);
  })
  .then(function(param){
    console.log(param);
    return async(6000);
  })
  .then(function(param){
    console.log(param);
  })
  .catch(function(error){
    console.log(error);
  });
  
  function foo() {
  return new Promise((resolve, reject) => {
    setTimeout(function(){
    resolve('A');
    }, 5000);
  });
}

・async/await

function test() {
  // 非同期処理
  return new Promise(function(resolve, reject){
    resolve(); // 成功。thenが呼ばれる
    reject(); // 失敗。catchが呼ばれる
  });
}

async function test2() {
  // 非同期処理の呼び出し。同期的に上から順に終わってから次の処理へ行く。
  await test();
  await test();
  return '成功';
}

test2().then(function() {
  // 成功時の処理
})
 .catch(function() {
   // 失敗時の処理
 });

非同期処理の不思議な所

必ず同期処理が終わった後に、非同期処理が完了するみたいだぞ。 ただし、非同期処理も裏では動いていて、同期処理が終わった瞬間、すでに完了済みの非同期処理も完了する。(意味不明な日本語だけどそんな感じなんだよ。)  下記コードを動かしてみるとよくわかる。  「8秒後に8000ミリ秒経過しました!と1と2」が同時に表示→その2秒後に3が表示

// 非同期処理
function sample() {
  setTimeout(before, 1000, 1);
  setTimeout(before,2000, 2);
  setTimeout(before, 10000, 3);
}

// 同期処理
function sleep(wait) {
  var start = new Date();
  // 指定ミリ秒間だけループさせる
  while (new Date() - start < wait);
  console.log(`${wait}ミリ秒経過しました!`);
}

function before(string) {
  console.log(string);
}

sample();
sleep(8000);
--------------------
(出力結果)
8000ミリ秒経過しました! 
1 
2
3

なぜコールバック関数で非同期処理を書くのか?

処理に順番をつけたいから。

  • 例1(順番を付けない)

下記コードはundeifinedが表示される。

なぜなら、必ず同期処理が終わってから非同期処理が完了するから。

つまり、同期処理であるconsole.log(hoge)が終わってから

→非同期処理のsetTimeout(function() {  hoge = 'hoge';}, 0);が完了するから。

let hoge; 
// 0秒後にhogeに文字列を代入する 
setTimeout(function() { 
  hoge = 'hoge'; 
}, 0); 
console.log(hoge);  // => undefined

非同期処理にコールバック関数で順番を付ける

  • 順番付けない例 1秒後に1→2秒後に2→3秒後に3が表示。合計3秒。 全てのsetTimeoutがほぼ同時に開始する。
function sample() { 
  setTimeout(before, 1000, 1); 
  setTimeout(before,2000, 2); 
  setTimeout(before, 3000, 3); 
}
  • 順番を付けた例 コールバックをネストする。 1秒後に1→その2秒後に2→その3秒後に3が表示。合計6秒。
setTimeout(function one() {
  console.log(1)
  setTimeout(function two(){
    console.log(2)
    setTimeout(function three() {
      console.log(3);
    }, 3000);
  },2000);
},1000);

つまり非同期処理を非同期じゃなくしてる感じ。 1が終わってから2、終わってから3。

非同期処理の何がうれしいのか?

重たい処理を非同期で裏でやってもろて、同期処理は先に進めとく。

setTimeout(function one() {
  console.log(1)
  setTimeout(function two(){
    console.log(2)
    setTimeout(function three() {
      console.log(3);
    }, 3000);
  },2000);
},1000);
console.log(4) //ここだけ同期処理
-----------------------
4
1
2
3

コールバック関数の引数の値ってどこからとってきてるの?

https://qiita.com/matsuby/items/3f635943f25e520b7c20#%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%AA%E3%81%84%E3%81%A8%E6%84%9F%E3%81%98%E3%82%8B%E6%B0%97%E6%8C%81%E3%81%A1%E6%82%AA%E3%81%95

例 fs.readFile() err,dataの値はいったどこからやってくるのか・・・。

fs.readFile('/etc/passwd', function(err, data) {
  if (err) throw err;
  console.log(data);
});

答え:高階関数(呼び出し元の関数)がとってくる

// showHogeに呼び出される高階関数 
// ここで引数に値を代入
function call(callback){ 
  let string = 'コールバック関数の引数に入る値'; 
  let string2 = ':2個目'; 
  callback(string, string2); 
} 
//ここだけ見ると、引数stringに何が入っているか謎。 
function showHoge() { 
  call(function(string , string2){ 
    console.log(string+string2); 
  }); 
} 
showHoge();  // => コールバック関数の引数に入る値:2個目

Promise

・参考 JavaScriptの非同期処理を理解する その2 〜Promise編〜

https://knowledge.sakura.ad.jp/24890/

JavaScript Promise の基本的な使い方

http://javascript.keicode.com/lang/promise-basics.php

・基本的な書き方

const promise = new Promise(function(resolve, reject) {
    //ここに非同期の重い処理を書く
    resolve('成功'); // 非同期処理成功時の処理
        reject('失敗'); // 非同期処理失敗時の処理
}

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

  • 待機pending: 初期状態。成功も失敗もしていません。
  • 満足fulfilled: 処理が成功して完了したことを意味します。
  • 拒絶rejected: 処理が失敗したことを意味します。

満足するとthen()が実行され 拒絶されるとcatch()が実行される。

function async(param){
  return new Promise(function(resolve, reject) {
  setTimeout(function(){
    param += 500;
    
    if(param >= 2000) {
      resolve(param);
    } else {
      reject('failed.')
    }
  }, 2000);
});
}

async(2000)
  .then(function(param){
    console.log(param);
    return async(4000);
  })
  .then(function(param){
    console.log(param);
    return async(6000);
  })
  .then(function(param){
    console.log(param);
  })
  .catch(function(error){
    console.log(error);
  });

・流れ Promise内の処理が実行

→処理の結果をparamに渡す

 →(満足)then内の処理が実行

 →(拒否)catch内の処理が実行

thenはその時って意味だから、async().then()はasync関数が完了したその時ってことだね。

・Promiseはコールバックの入れ子構造

resolve/rejectにも関数が入る。

Promise(function(resolve, reject)

Promise(コールバック関数1(コールバック関数1-1, コールバック関数1-2)という入れ子構造。

  • コールバック関数1にはthen/catchが入る
  • コールバック関数1-1にはthenの中のコールバック関数が入る。
  • コールバック関数1-2にはcatchの中のコールバック関数が入る。

Promiseの理解が深まるかもしれないちょっと不思議な奴

return foo(B)ではなく、return 'B';なとこごポイント。 これはプロミスは1回しか呼んでないので、returnの値がそのまま次のthenに入ってるんだと思う。

function foo() { 
  return new Promise((resolve, reject) => { 
    resolve('A') 
  }) 
} 
foo() 
  .then((value) => { 
    console.log(`1st: ${value}`) 
    return 'B' 
  }) 
  .then((value) => { 
    console.log(`2nd: ${value}`) 
    return 'C' 
  }) 
  .then((value) => { 
    console.log(`3rd: ${value}`) 
  }) 
  .catch((reason) => { 
    console.log('catch!') 
    console.log(reason) 
  }) 
// 1st: A 
// 2nd: B 
// 3rd: C

Promise のチェインニング https://javascript.keicode.com/lang/promise-basics.php#4

・疑問 thenのreturn値が次のthenの引数に入っている。 これはthenに限らず、普通のメソッドチェーンでもそういうものなの? たとえばJavaではこんな風に書くけど引数は普通に指定してるもんね。

String str1 = "abc123abc123"; 
boolean str2 = str1.replace("abc", "0").contains("0");
System.out.println(str2); // true

Promiseの理解が深まるかもしれないちょっと不思議な奴2

下記は一見すると、thenのどこかで3未満になったらFailedを返すように見える。 しかし、実はそうではない。 getNumberが呼ばれているのが最初の1回だけなのがポイント。 つまり、Promiseが呼ばれているのも最初の1回だけ。 だから、最初の1回だけ if (num >= 3) の判定をしている。 だから最初が3以上なら、その後3未満になってもrejectにならない。

function getNumber(num) {
    return new Promise(function(resolve, reject) {
        // numが3以上ならnumを返し、3未満なら"Falied!"のメッセージを返す
        if (num >= 3) {
            setTimeout(function() {
                resolve(num);
            }, 1000);
        } else {
            reject("Falied!");    
        }
    });  
}

// 今回は3を渡しているので、resolveから3が返ってくる
getNumber(3).then(function(result) {
    console.log(result);
    //numに3を加算して、getNumberに返している
    return result + 3;
}).then(function(result) {
    //上と同じ処理の繰り返し。これがチェイン
    console.log(result);
    return result - 6;
}).then(function(result) {
    // 最終結果として、numに6を加算した数を表示
    console.log(result);
})
  .catch(function(error){
    // 3未満の場合はrejectが呼び出され、"Falied!"のメッセージが表示される
    console.log(error);
  });
-----------
3
6
0

もし、上記を修正してthenの途中でもエラー判定をするなら。 return result +3; →return getNumber(result + 3); に変更する。

getNumber(3).then(function(result) {
    console.log(result);
    //変更箇所。getNumberを呼び出している。
    return getNumber(result + 3);
}).then(function(result) {
    console.log(result);
    //変更箇所。getNumberを呼び出している。ここでエラー。
    return getNumber(result - 6);
}).then(function(result) {
    console.log(result);
})
  .catch(function(error){
    // 3未満の場合はrejectが呼び出され、"Falied!"のメッセージが表示される
    console.log(error);
  });
--------------
3
6
Failed!

Prmiseの例外処理

・catch/finallyをつけられる。

function async(param){ 
  return new Promise(function(resolve, reject) { 
  setTimeout(function(){ 
    param += 500; 
     
    if(param >= 2000) { 
      resolve(param); 
    } else { 
      reject('failed.') 
    } 
  }, 2000); 
}); 
} 
async(2000) 
  .then(function(param){ 
    console.log(param); 
    return async(4000); 
  }) 
  .then(function(param){ 
    console.log(param); 
    return async(6000); 
  }) 
  .then(function(param){ 
    console.log(param); 
  }) 
  .catch(function(error){ 
    console.log(error); 
  })
  .finally(function() {
   console.log('ファイナリー');
 })

Promiseの関数

・参考 Primiseの静的メソッド https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise#static_methods

Promise.all 全てのPromiseがresolveする、または1つでもrejectするとPromiseが返ってくる。 戻り値は配列。

unction one(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(1000);
  }, 1000);
  });
}

function two(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(2000);
  }, 2000);
  });
}

function three(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    reject(3000);
  }, 3000);
  });
}

Promise.all([one(), two(), three()])
 .then(function(values){
   console.log(values);
 })
 .catch(function() {
   console.log('rejectだよ。');
 })
---------
1000, 2000, 3000

Promise.race Primise1つでもresolve/rejectするとPromiseを返す。

function one(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    reject(1000);
  }, 1000);
  });
}

function two(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(2000);
  }, 2000);
  });
}

function three(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(3000);
  }, 3000);
  });
}

const promises = [one(), two(), three()];

Promise.race([one(), two(), three()])
 .then(function(values){
     console.log(values);
 });
 .catch(function() {
   console.log('rejectだよ。');
 });
------------
rejectだよ。

Promise.any Promiseが1つでもresolveする、または全てrejectするとPromiseを返す。 まだ実験段階で、対応ブラウザ少ない。

function one(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    reject(1000);
  }, 1000);
  });
}

function two(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(2000);
  }, 2000);
  });
}

function three(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(3000);
  }, 3000);
  });
}

const promises = [one(), two(), three()];

Promise.any([one(), two(), three()])
 .then(function(values){
     console.log(values);
 })
 .catch(function() {
   console.log('rejectだよ。');
 });
----------------
2000

Promise.reject Promiseが成功か失敗か関係なく、即rejectを返すっぽい。テスト用?

function one(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(1000);
  }, 1000);
  });
}

function two(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(2000);
  }, 2000);
  });
}

function three(){
  return new Promise(function(resolve, reject){
    setTimeout(() => {
    resolve(3000);
  }, 3000);
  });
}

const promises = [one(), two(), three()];

Promise.reject([one(), two(), three()])
 .then(function(values){
     console.log(values);
 })
 .catch(function() {
   console.log('rejectだよ。');
 });

Promiseのよくわからないところ

function wait(time){
  const start = Date.now();
  while(Date.now() - start < time){
    }
  console.log(time + ' wait');
  }
  
  function waitAsync(time){
    return new Promise(function(resolve, reject) {
        const start = Date.now();
  while(Date.now() - start < time){
    }
  resolve(time);
    });
  }
  
   function waitAsync2(time){
    return new Promise(function(resolve, reject) {
        const start = Date.now();
  while(Date.now() - start < time){
    }
  resolve(time);
    });
  }

 waitAsync(1500)
  .then((time) => {
    console.log(time +' waitAsync')
  })

 waitAsync2(1000)
  .then((time) => {
    console.log(time + ' waitAsync2')
  })

wait(2000);
--------------
2000 wait 
1500 waitAsync 
1000 waitAsync2

なんでこんな順番になるんや・・・。 waitAsyncの処理で1.5秒経過(イベントキューに入る)→waitAsync2の処理で1秒経過(イベントキューに入る)→waitの処理で2秒経過、完了→waitAsync完了→waitAsync2完了ということ? イベントキューはイベントの待ち行列のこと。

https://knowledge.sakura.ad.jp/24888/

async/await

・参考 async/await 入門(JavaScripthttps://qiita.com/soarflat/items/1a9613e023200bbebcb3

・基本的な書き方

function test() {
  // 非同期処理
  return new Promise(function(resolve, reject){
    resolve(); // 成功。thenが呼ばれる
    reject(); // 失敗。catchが呼ばれる
  });
}

async function test2() {
  // 非同期処理の呼び出し。同期的に上から順に終わってから次の処理へ行く。
  await test();
  await test();
  return '成功';
}

test2().then(function() {
  // 成功時の処理
})
 .catch(function() {
   // 失敗時の処理
 });

・中身解説

// resolve1!!をreturnしているため、この値がresolveされる 
async function resolveSample() { 
    return 'resolve!!'; 
} 
// resolveSampleがPromiseを返し、resolve!!がresolveされるため 
// then()が実行されコンソールにresolve!!が表示される 
resolveSample().then(value => { 
    console.log(value); // => resolve!! 
}); 
// reject!!をthrowしているため、この値がrejectされる 
async function rejectSample() { 
    throw new Error('reject!!'); 
} 
// rejectSampleがPromiseを返し、reject!!がrejectされるため 
// catch()が実行されコンソールにreject!!が表示される 
rejectSample().catch(err => { 
    console.log(err); // => reject!! 
});

awaitとは

async function内でPromiseの結果(resolve、reject)が返されるまで待機する(処理を一時停止する)演算子のこと。 以下のように、関数の前にawaitを指定すると、その関数のPromiseの結果が返されるまで待機する。

async function sample(){
    const result = await sampleResolve();
    
    // sampleResolve()のPromiseの結果が返ってくるまで以下は実行されない
    console.log(result);}

awaitは何をするのか

  • awaitを指定した関数のPromiseの結果が返されるまで、async function内の処理を一時停止する。
  • 結果が返されたらasync function内の処理を再開する。

async/awaitのメリット

メリット1、thenを繋げないで済む。

・Promiseの例 thenが長い。

function async(param){ 
  return new Promise(function(resolve, reject) { 
  setTimeout(function(){ 
    resolve(param);
  }, 2000); 
}); 
} 
async(2000) 
  .then(function(param){ 
    console.log(param); 
    return async(4000); 
  }) 
  .then(function(param){ 
    console.log(param); 
    return async(6000); 
  }) 
  .then(function(param){ 
    console.log(param); 
  }) 
  .catch(function(error){ 
    console.log(error); 
  });

・async/awaitの例 then内で関数を呼ばず、awaitで関数を繰り返し呼ぶ。

function aaa(value) {
   return new Promise((resolve, reject) => {
     setTimeout(() => {
       resolve(value);
   }, 1000);
   });
 }
 async function bbb() {
   await aaa(1000);
   console.log(1000);
   
   await aaa(2000);
   console.log(2000);
   
   await aaa(3000);
   return 3000;
 }

bbb().then((value) => {
  console.log(value);
})
 .catch((value) => {
   console.log('fail');
 });

メリット2、ループ文で繰り返し実行できる

function sampleResolve(value) { 
    return new Promise(resolve => { 
        setTimeout(() => { 
            resolve(value); 
        }, 1000); 
    }) 
} 
async function sample() { 
    for (let i = 0; i < 5; i += 1) { 
        const result = await sampleResolve(i); 
        console.log(result); 
    } 
    return 'ループ終わった。' 
} 
sample().then((v) => { 
    console.log(v); // => 0 
                    // => 1 
                    // => 2 
                    // => 3 
                    // => 4 
                    // => ループ終わった。 
});

async/awaitの間違えやすそうなところ

同期処理が先に完了するので、let d = bbb(); はからっぽ。 thenしてあげよう。

function aaa(value) {
   return new Promise((resolve, reject) => {
       resolve(value);
   });
 }
 async function bbb() {
   const a = await aaa(1000);
   const b = await aaa(2000);
   const c =  await aaa(3000);
   return a + b + c;
 }
let d = bbb();
 console.log(d); // からっぽ
 bbb().then((value) => {
  console.log(value);
}); // こっちは6000

Promiseの戻り値には何が入ってるの?

resolve()の引数の値が入るみたい。

function aaa() {
   return new Promise((resolve, reject) => {
       resolve('abc');
   });
 }
 async function bbb() {
  const a = await aaa();
  console.log(a); // abc
 }
bbb();

なぜAjaxは画面更新せずに画面変更できるのか

よく考えたらイベントも画面更新してないよね・・・。 というかJavaScriptって基本画面更新しないよね。

btn.onclick = function() { 要素.style.color = 'red'; }

Ajaxもこれと同じだね。 Ajaxの場合はサーバーとの通信が入っているってだけだね。 なおかつ非同期なので、ユーザーは通信を待たずに済むね。 普通の通信はHTMLを丸ごと入れ替えてるから画面更新が入るね。ね。 ・初心者目線でAjaxの説明 https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9