JavaScript
初心者
es6
es2015
非同期関数

非同期関数を配列で同期的に実行するようにしてみる

More than 1 year has passed since last update.


前書き

非同期関数配列同期的に管理したい時があって、Promiseだけでは何か物足りなかったので、非同期関数を管理するクラスを書こうと思います。あと、プログラミングの練習も兼ねて。

今回書くクラスや関数はES6(ES2015)で書くので、わからない人は以下の記事がとても参考になるかと思います。ES5でクラス書くのがめんどくさい

参考になった記事


コード


function isFunction(func){
return Object.prototype.toString.call(func) === "[object Function]";
}

class Permutation{
constructor(methods){
if( !Array.isArray(methods) ){ //渡されたmethodsが配列じゃ無かったら、エラーを吐く。
console.error('配列を指定してください');
}

for(let i = methods.length - 1; i >= 0; --i){ // 配列からいらない値を削除
if( Array.isArray( methods[i] ) ){ // 配列なら
for(let j = methods[i].length - 1; j >= 0; --j) {
if( !isFunction( methods[i][j] ) ){ // 関数じゃないのがあれば、
methods[i].splice(j,1); // 配列から削除
}
}
} else if( !isFunction( methods[i] ) ){ // 関数じゃないのがあれば、
methods.splice(i,1); //配列から削除
}
}

this.methods = methods; // 非同期関数を保持
this.return_value = []; // 非同期関数から渡された値を保持
this.isProgress = false; // 関数が実行しているかのフラグ
this.parallel = false; // 並列処理が実行中かどうかのフラグ
this.result = true; // 実行結果を保持(失敗するとfalse)
this.error_func = null; // エラー処理を保持
this.finish_func = null; // 終了処理を保持
this.is_finish = false; // 全処理終了フラグ

// 非同期関数に渡すコールバック
this.permutation = {
get : this.Get.bind(this),
run : this.Run.bind(this),
error : this.onError.bind(this),
catch : this.catch.bind(this),
finish: this.finish.bind(this),
};
}

// 前の非同期関数から渡された値を返す。
Get(){
return this.return_value.slice(0); // 参照させないようにする。
}

// 非同期関数内で実行する関数
// この関数に値を渡すと、次に実行する関数は、get関数から取得できる。
Run(...result){

if(this.is_finish) return ; //終了フラグが上がっていたら処理しない。

if( this.parallel ){ //並列処理中ならば

if( result.length > 0 ){
let index = this.return_value.length;
if( !Array.isArray(this.return_value[index]) ){ // 並列の結果は配列で囲む。
this.return_value[index] = [];
}

if( result.length == 1 ){
this.return_value[index].push(result[0]);
}else{
this.return_value[index].push(result); //配列にまとめて保持。
}
}

--this.isProgress; // 終了した関数をカウント
if(this.isProgress > 0 ){ // まだ処理している関数があれば、
return ; //並列処理へ戻る。
}
this.isProgress = false;
this.parallel = false; // 並列処理終了
}

if(result.length > 0){
if(result.length == 1){
this.return_value.push(result[0]);
}else{
this.return_value.push(result); //配列にまとめて保持。
}
}

if(this.isProgress){
this.isProgress = false; // 実行終了
}

let callback = this.methods.shift(); // 非同期関数を取得

if( Array.isArray(callback) ){ //配列ならば、
this.All(callback); //並列処理へ
return ;
}

if( callback && this.result ){ // 非同期関数があってエラーがなければ、
this.isProgress = true; // 実行開始
callback(this.permutation); //コールバック関数を非同期関数に渡して実行。
}else if( callback && !this.result ){ //エラーなら、
this.Error(); // エラー処理へ
} else { // 関数が無くなったら、
this.finish(); // 終了処理へ
}
}

// エラー処理を定義
catch( callback = null ){
this.error_func = callback;
return this; // このクラスを返す。
}

// エラー・例外処理
onError(...args){
this.result = false; // 結果をエラーに変更これ以降の非同期関数は実行されない。
this.parallel = false; //並列処理強制終了

if(this.error_func){ //エラー処理が定義されて入れば、
this.error_func(...args,this.permutation.get()); // エラー内容と、これまで取得した結果を渡してエラー処理実行
}
}

// 終了処理を定義
setFinish( callback = null ){
this.finish_func = callback;
}

// 終了処理を実行これ以降配列の関数は実行されない。
finish(){
if(this.finish_func){
this.finish_func(this.permutation.get());
}

this.is_finish = true;
}

// 並列処理
All(callbacks){
this.parallel = true; // 並列処理開始
this.isProgress = callbacks.length; // 並列処理する関数の数を記憶
for(let i in callbacks){
callbacks[i](this.permutation); // 非同期関数実行
}
}

// 実行しやすくする関数
start(){
this.Run();
return this; //このクラスを返す。
}
}


使い方 & 解説 

今回作ったPermutationクラスは、配列に入っている関数を配列のindex(指数)が低い順に同期的に実行するクラスです。


例 1

まずは、非同期関数を定義してみます。


function Test(){
setTimeout(() => {
console.log('3秒経過しました。');
},3000); // 3秒後に実行
}

function Test2(){
setTimeout(() => {
console.log('5秒経過しました。');
},5000); // 5秒後に実行
}

function Test3(){
setTimeout(() => {
console.log('7秒経過しました。');
},7000); // 7秒後に実行
}

この非同期関数Test,Test2,Test3は,それぞれコンソールに経過時間を出力します。これらを以下のように実行してみます。


Test3();
Test2();
Test();

通常なら、Test3() → Test2() → Test()ように上に書かれている関数から実行されますが、これらは非同期関数なので、Test() → Test2() → Test3()のように設定した秒数後に実行されコンソールには、以下のように出力されます。



3秒経過しました。

5秒経過しました。

7秒経過しました。

この結果をPermutationクラスを用いて同期的に実行してみます。


例 2


function Test(per){
setTimeout(() => {
console.log('3秒経過しました。');
per.run();
},3000); // 3秒後に実行
}

function Test2(per){
setTimeout(() => {
console.log('5秒経過しました。');
per.run();
},5000); // 5秒後に実行
}

function Test3(per){
setTimeout(() => {
console.log('7秒経過しました。');
per.run();
},7000); // 7秒後に実行
}

let func_list = [Test3,Test2,Test]; //配列に入れる。

let per = new Permutation(func_list); //Permutationクラスをインスタンス化

per.start(); // 関数を実行

上記の結果は、以下のようになります。



7秒経過しました。

5秒経過しました。

3秒経過しました。

それでは、解説していきます。

まず、非同期関数をPermutationで実行するとき、非同期関数には、Permutationクラスから引数に関数が入ったobjectを受け取ります。具体的には以下のようなものです。


{
get : 前の関数から受け取った値を取得する関数。
run : 処理終了時に次の関数を実行するコールバック。
error : catchで定義したエラー処理を実行するコールバック。(これ以降の関数は実行されないでcatchで定義した関数を実行する。)
catch : errorで実行する処理を定義する。
finish: 全処理完了時のコールバック。 (これ以降関数は、実行しない。)
}

これらの渡された関数を非同期関数内で呼び出す必要があります。特にrun関数は、呼び出さないと次の関数が実行されないので、必ず実行するようにしています。


run関数

run関数の引数に値を渡すと、後に実行される関数はget関数でその値を取得することができます。

引数を複数設定した場合は、配列で取得できます。


function Test(per){
setTimeout(()=>{
console.log("3秒経過しました。",per.get());
per.run("Hello World"); // Test2に値を渡す。
},3000);
}

function Test2(per){
setTimeout(()=>{
console.log( "Testの内容 → ",per.get() );
per.run("hoge hoge","foo bar");
},1000);
}

function Test3(per){
setTimeout(()=>{
console.log( "TestとTest2の内容 → ",per.get() );
per.run();
},5000);
}

let per = new Permutation([Test,Test2,Test3]);

per.start();

上記の結果は、以下のようになります。



3秒経過しました。 []

Testの内容 → [ "Hello World" ]

TestとTest2の内容 → [ "Hello World",[ "hoge hoge","foo bar" ] ]


error関数

error関数は、catch関数で定義した関数を実行します。catch関数で定義した関数の引数にはerror関数で渡した時の値とget関数で取得できる値が渡されます。


function Test( per ){
setTimeout(() => {
per.run("YOLO!!");
},3000);
}

function Test2( per ){
setTimeout(() => {
per.error("エラーを発生させてみる。",false);
},4000);
}

function Test3(per){ // この関数は、Test2がerrorを出した後なので実行されない。
setTimeout(() => {
console.log("Hello Hoge");
per.run();
},10000);
}

let per = new Permutation([Test,Test2]);
per.start()
.catch( ( message, status, get_list )=> {
console.log( "messageは、" + message, "statusは、" + status, "前の関数が渡した値は、" + get_list );
})

// catchやfinishはメソッドチェーンでかける。

上記の結果は、以下のようになります。



messageは、エラーを発生させてみる。 statusは、false 前の関数が渡した値は、YOLO!


finish関数

finish関数は、配列にある全ての関数が終了した時か、明示的に非同期関数内でfinish関数を実行した時にsetFinishで定義した関数が実行されます。引数にはget関数で取得できる値のみが渡されます。

この関数が実行された後は、配列にある関数は実行されません。


function Test( per ){
setTimeout(()=>{
console.log("3秒経過しました。 Test");
per.run("test");
},3000);
}

function Test2( per ){
setTimeout(()=>{
console.log("5秒経過しました。 Test2");
per.run("test2");
},5000);
}

let per = new Permutation([ Test2,Test ]);

per.start()
.setFinish(( get_list )=>{
console.log("Finish!!! get_list →",get_list);
});

// setFinishは、メソッドチェーンでしか定義できません。

上記の結果は、以下のようになります。



5秒経過しました。 Test2

3秒経過しました。 Test

Finish!!! get_list → ["test2", "test"]


並列処理

配列内にある関数を、配列で囲むと囲んだ関数は並列実行されます。


function Test( per ){
setTimeout(()=>{
console.log("3秒経過しました。 Test");
per.run();
},3000);
}

function Test2( per ){
setTimeout(()=>{
console.log("5秒経過しました。 Test2");
per.run();
},5000);
}

let per = new Permutation([ [Test2,Test], Test2,Test ]);

per.start();

上記の結果は、以下のようになります。



3秒経過しました。 Test

5秒経過しました。 Test2

5秒経過しました。 Test2

3秒経過しました。 Test


感想

疲れた(;´д`)