web-dev-qa-db-ja.com

JavaScriptの再帰的約束

リンクの最終リダイレクトURLを見つけるJavascript Promiseを書いています。

私がやっていることは、HEADを使用してPromiseXMLHttpRequestリクエストを行うことです。次に、ロード時に、HTTPステータスで300の範囲内の何かを確認します。または、オブジェクトにresponseURLが添付されており、そのURLが片手で渡されたものと異なる場合。

どちらも当てはまらない場合は、resolve(url)です。それ以外の場合、応答URLでgetRedirectUrl()resolve()を再帰的に呼び出します。

私のコードは次のとおりです。

_function getRedirectUrl(url, maxRedirects) {
    maxRedirects = maxRedirects || 0;
    if (maxRedirects > 10) {
        throw new Error("Redirected too many times.");
    }

    var xhr = new XMLHttpRequest();
    var p = new Promise(function (resolve) {
        xhr.onload = function () {
            var redirectsTo;
            if (this.status < 400 && this.status >= 300) {
                redirectsTo = this.getResponseHeader("Location");
            } else if (this.responseURL && this.responseURL != url) {
                redirectsTo = this.responseURL;
            }

            if (redirectsTo) {
                // check that redirect address doesn't redirect again
                // **problem line**
                p.then(function () { self.getRedirectUrl(redirectsTo, maxRedirects + 1); });
                resolve();
            } else {
                resolve(url);
            }
        }

        xhr.open('HEAD', url, true);
        xhr.send();
    });

    return p;
}
_

次に、この関数を使用するには、次のようにします。

_getRedirectUrl(myUrl).then(function (url) { ... });
_

問題は、getRedirectUrl内のresolve();getRedirectUrl再帰呼び出しを呼び出す前に呼び出し元の関数からthen()を呼び出し、その時点で、 URLはundefinedです。

p.then(...getRedirectUrl...)return self.getRedirectUrl(...)するのではなく試みましたが、これは決して解決しません。

私が推測しているのは、私が使用しているパターン(基本的にその場で思いついたもの)が正しくないということです。

33
dx_over_dt

問題は、getRedirectUrl()から返されるプロミスには、URLに到達するためのロジックのチェーン全体を含める必要があるということです。最初のリクエストの約束を返すだけです。関数の中で使用している.then()は何もしていません。

これを修正するには:

リダイレクトのためにredirectUrlに解決されるプロミスを作成します。

_var p = new Promise(function (resolve) {
    var xhr = new XMLHttpRequest();

    xhr.onload = function () {
        var redirectsTo;

        if (xhr.status < 400 && xhr.status >= 300) {
            redirectsTo = xhr.getResponseHeader("Location");
        } else if (xhr.responseURL && xhr.responseURL != url) {
            redirectsTo = xhr.responseURL;
        }

        resolve(redirectsTo);
    };

    xhr.open('HEAD', url, true);
    xhr.send();
});
_

that.then()を使用して、必要に応じて再帰呼び出しを返すかどうかを返します。

_return p.then(function (redirectsTo) {
    return redirectsTo
        ? getRedirectUrl(redirectsTo, redirectCount+ 1)
        : url;
});
_

完全なソリューション:

_function getRedirectUrl(url, redirectCount) {
    redirectCount = redirectCount || 0;
    if (redirectCount > 10) {
        throw new Error("Redirected too many times.");
    }

    return new Promise(function (resolve) {
        var xhr = new XMLHttpRequest();

        xhr.onload = function () {
            var redirectsTo;

            if (xhr.status < 400 && xhr.status >= 300) {
                redirectsTo = xhr.getResponseHeader("Location");
            } else if (xhr.responseURL && xhr.responseURL != url) {
                redirectsTo = xhr.responseURL;
            }

            resolve(redirectsTo);
        };

        xhr.open('HEAD', url, true);
        xhr.send();
    })
    .then(function (redirectsTo) {
        return redirectsTo
            ? getRedirectUrl(redirectsTo, redirectCount+ 1)
            : url;
    });
}
_
43
JLRishe

簡単なソリューションを次に示します。

const recursiveCall = (index) => {
    return new Promise((resolve) => {
        console.log(index);
        if (index < 3) {
            return resolve(recursiveCall(++index))
        } else {
            return resolve()
        }
    })
}

recursiveCall(0).then(() => console.log('done'));
6
cuddlemeister

多くのプログラミング言語で行ったように、与えられた数のfactorialを返す以下の例を確認してください。

JavaScript promiseを使用して以下の例を実装しました。

let code = (function(){
        let getFactorial = n =>{
                return new Promise((resolve,reject)=>{
                        if(n<=1){
                                resolve(1);
                        }
                        resolve(
                                getFactorial(n-1).then(fact => {
                                        return fact * n;
                                })
                        )
                });
        }
        return {
                factorial: function(number){
                        getFactorial(number).then(
                                response => console.log(response)
                        )
                }
        }
})();
code.factorial(5);
code.factorial(6);
code.factorial(7);
4
Mayur S

次の2つの機能があります。

  • _getRedirectUrl-リダイレクトされたURLのシングルステップルックアップを検索するためのsetTimeoutオブジェクトシミュレーションです(これは、XMLHttpRequest HEADリクエストの単一インスタンスに相当します)
  • getRedirectUrl-再帰URL Promisesを呼び出してリダイレクトURLを検索します

秘密のソースは、サブPromiseであり、そのサブミットが成功すると、親プロミスからresolve()の呼び出しがトリガーされます。

_function _getRedirectUrl( url ) {
    return new Promise( function (resolve) {
        const redirectUrl = {
            "https://mary"   : "https://had",
            "https://had"    : "https://a",
            "https://a"      : "https://little",
            "https://little" : "https://lamb",
        }[ url ];
        setTimeout( resolve, 500, redirectUrl || url );
    } );
}

function getRedirectUrl( url ) {
    return new Promise( function (resolve) {
        console.log("* url: ", url );
        _getRedirectUrl( url ).then( function (redirectUrl) {
            // console.log( "* redirectUrl: ", redirectUrl );
            if ( url === redirectUrl ) {
                resolve( url );
                return;
            }
            getRedirectUrl( redirectUrl ).then( resolve );
        } );
    } );
}

function run() {
    let inputUrl = $( "#inputUrl" ).val();
    console.log( "inputUrl: ", inputUrl );
    $( "#inputUrl" ).prop( "disabled", true );
    $( "#runButton" ).prop( "disabled", true );
    $( "#outputLabel" ).text( "" );
    
    getRedirectUrl( inputUrl )
    .then( function ( data ) {
        console.log( "output: ", data);
        $( "#inputUrl" ).prop( "disabled", false );
        $( "#runButton" ).prop( "disabled", false );
        $( "#outputLabel").text( data );
    } );

}_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Input:

<select id="inputUrl">
    <option value="https://mary">https://mary</option>
    <option value="https://had">https://had</option>
    <option value="https://a">https://a</option>
    <option value="https://little">https://little</option>
    <option value="https://lamb">https://lamb</option>
</select>

Output:

<label id="outputLabel"></label>

<button id="runButton" onclick="run()">Run</button>_

再帰的約束の別の例として、迷路を解決するためにそれを使用しました。 Solve()関数は、解決策の1ステップを迷路に進めるために再帰的に呼び出されます。 setTimeout関数を使用して、ソリューションのアニメーションをフレームごとに100ms(つまり、10hzフレームレート)に設定します。

_const MazeWidth = 9
const MazeHeight = 9

let Maze = [
    "# #######",
    "#   #   #",
    "# ### # #",
    "# #   # #",
    "# # # ###",
    "#   # # #",
    "# ### # #",
    "#   #   #",
    "####### #"
].map(line => line.split(''));

const Wall = '#'
const Free = ' '
const SomeDude = '*'

const StartingPoint = [1, 0]
const EndingPoint = [7, 8]

function PrintDaMaze()
{
    //Maze.forEach(line => console.log(line.join('')))
    let txt = Maze.reduce((p, c) => p += c.join('') + '\n', '')
    let html = txt.replace(/[*]/g, c => '<font color=red>*</font>')
    $('#mazeOutput').html(html)
}

function Solve(X, Y) {

    return new Promise( function (resolve) {
    
        if ( X < 0 || X >= MazeWidth || Y < 0 || Y >= MazeHeight ) {
            resolve( false );
            return;
        }
        
        if ( Maze[Y][X] !== Free ) {
            resolve( false );
            return;
        }

        setTimeout( function () {
        
            // Make the move (if it's wrong, we will backtrack later)
            Maze[Y][X] = SomeDude;
            PrintDaMaze()

            // Check if we have reached our goal.
            if (X == EndingPoint[0] && Y == EndingPoint[1]) {
                resolve(true);
                return;
            }

            // Recursively search for our goal.
            Solve(X - 1, Y)
            .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X + 1, Y);
            } )
            .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X, Y - 1);
             } )
             .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X, Y + 1);
             } )
             .then( function (solved) {
                 if (solved) {
                     resolve(true);
                     return;
                 }

                 // Backtrack
                 setTimeout( function () {
                     Maze[Y][X] = Free;
                     PrintDaMaze()
                     resolve(false);
                 }, 100);
                 
             } );

        }, 100 );
    } );
}

Solve(StartingPoint[0], StartingPoint[1])
.then( function (solved) {
    if (solved) {
        console.log("Solved!")
        PrintDaMaze()
    }
    else
    {
        console.log("Cannot solve. :-(")
    }
} );_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<pre id="mazeOutput">
</pre>_
1
Stephen Quan

async/awaitをサポートする環境にいる場合(事実上すべての現代の環境がサポートしています)、再帰関数パターンに少し似ている_async function_を書くことができますそして愛。 Promiseの性質上、XMLHttpRequestイベントを介してのみ値を取得するため(load自体を公開するのではなく)、Promiseを完全に回避することはできませんが、再帰的呼び出しを行う関数の性質はおなじみのはずです。

この質問を最初に書いたときよりも4年以上JavaScriptの経験があったので、コードを少し整理しましたが、基本的には同じように機能します。

_// creates a simple Promise that resolves the xhr once it has finished loading
function createXHRPromise(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();

        // addEventListener('load', ...) is basically the same as setting
        // xhr.onload, but is better practice
        xhr.addEventListener('load', () => resolve(xhr));

        // throw in some error handling so that the calling function 
        // won't hang
        xhr.addEventListener('error', reject);
        xhr.addEventListener('abort', reject);

        xhr.open('HEAD', url, true);
        xhr.send();
    });
}

async function getRedirectUrl(url, maxRetries = 10) {
    if (maxRetries <= 0) {
        throw new Error('Redirected too many times');
    }

    const xhr = await createXHRPromise(url);
    if (xhr.status >= 300 && xhr.status < 400) {
        return getRedirectUrl(xhr.getResponseHeader("Location"), maxRetries - 1);
    } else if (xhr.responseURL && xhr.responseURL !== url) {
        return getRedirectUrl(xhr.responseURL, maxRetries - 1);
    }

    return url;
}
_

async/awaitの簡単な説明

  • _async function_はPromiseの構文糖衣です
  • awaitPromise.then()の構文糖衣です
  • return _async function_内はresolve()の構文糖衣です
  • throw _async function_内はreject()の構文糖衣です

_async function_が別の_async function_呼び出しまたはPromiseのいずれかを返す場合、元の呼び出しが解決する前に、Promiseを解決するのとまったく同じ方法で関数/約束が解決しますPromiseパターン。

したがって、元の質問とまったく同じ方法でgetRedirectUrl(someUrl).then(...).catch(...)を呼び出すことができます。

XHRを使用してリダイレクトされたURLを解決すると、適切なCORSヘッダーが含まれていないURLに対して失敗することに注意してください。


追加のボーナスとして、async/awaitは反復アプローチを簡単にします。

_async function getRedirectUrl(url, maxRetries = 10) {
    for (let i = 0; i < maxRetries; i++) {
        const xhr = await createXHRPromise(url);
        if (xhr.status >= 300 && xhr.status < 400) {
            url = xhr.getResponseHeader("Location");
        } else if (xhr.responseURL && xhr.responseURL !== url) {
            url = xhr.responseURL;
        } else {
            return url;
        }
    }

    throw new Error('Redirected too many times');
}
_
1
dx_over_dt