web-dev-qa-db-de.com

Rekursives Versprechen in Javascript

Ich schreibe ein Javascript Promise, das die endgültige Weiterleitungs-URL eines Links ermittelt.

Was ich mache, ist eine HEAD-Anforderung in einer Promise mit einer XMLHttpRequest. Überprüfen Sie dann beim Laden den HTTP-Status auf etwas im Bereich 300 oder, wenn ihm eine responseURL an das Objekt angehängt ist und diese URL sich von derjenigen unterscheidet, für die sie übergeben wurde. 

Wenn keine der beiden zutreffend ist, resolve(url). Ansonsten rufe ich rekursiv getRedirectUrl() für die Antwort-URL und resolve() auf.

Hier ist mein Code:

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;
}

Um diese Funktion zu nutzen, mache ich so etwas:

getRedirectUrl(myUrl).then(function (url) { ... });

Das Problem ist, dass resolve(); in getRedirectUrl die then() von der aufrufenden Funktion aufruft, bevor es den rekursiven Aufruf getRedirectUrl aufruft, und an diesem Punkt ist die URL undefined.

Ich habe versucht, anstatt p.then(...getRedirectUrl...), return self.getRedirectUrl(...) zu tun, aber das wird sich niemals lösen.

Meine Vermutung ist, dass das Muster, das ich verwende (das ich im Grunde spontan erfunden habe), insgesamt nicht richtig ist.

22
dx_over_dt

Das Problem ist, dass das Versprechen, das Sie von getRedirectUrl() zurückgeben, die gesamte Logikkette enthalten muss, um zur URL zu gelangen. Sie geben gerade ein Versprechen für die allererste Anfrage zurück. Die .then(), die Sie mitten in Ihrer Funktion verwenden, tut nichts.

Um das zu beheben: 

Erstellen Sie ein Versprechen, das in redirectUrl für eine Weiterleitung aufgelöst wird oder sonst nichts:

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();
});

Verwenden Sie .then() für that , um den rekursiven Aufruf je nach Bedarf zurückzugeben oder nicht:

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

Komplettlösung:

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;
    });
}
32
JLRishe

Bitte überprüfen Sie das Beispiel unten, es wirdfactorialeiner gegebenen Zahl zurückgegeben, wie wir es in vielen Programmiersprachen getan haben.

Ich habe das folgende Beispiel mitJavaScriptpromises implementiert.

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);

3
Mayur Shedage

Folgendes hat zwei Funktionen:

  • _getRedirectUrl - Dies ist eine setTimeout-Objektsimulation zum Nachschlagen einer umgeleiteten URL in einem Schritt (dies entspricht einer einzelnen Instanz Ihrer XMLHttpRequest HEAD= request)
  • getRedirectUrl - rekursive Aufrufe Verspricht, die Umleitungs-URL nachzuschlagen

Die geheime Soße ist das Unter-Versprechen, dessen erfolgreicher Abschluss einen Aufruf zum Auflösen () des übergeordneten Versprechens auslöst.

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>

Als ein weiteres Beispiel für rekursive Versprechen habe ich es verwendet, um ein Labyrinth zu lösen. Die Solve() -Funktion wird rekursiv aufgerufen, um einen Schritt in einer Lösung zu einem Labyrinth vorzurücken, andernfalls wird ein Rücksprung ausgeführt, wenn sie auf eine Sackgasse stößt. Die Funktion setTimeout wird verwendet, um die Animation der Lösung auf 100 ms pro Bild (d. H. 10-Hz-Bildrate) einzustellen.

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

Wenn Sie sich in einer Umgebung befinden, die async/await unterstützt (in praktisch allen modernen Umgebungen), können Sie ein async function Schreiben, das eher einem rekursiven Funktionsmuster ähnelt wir alle kennen und lieben. Es ist nicht möglich, ein Promise vollständig zu vermeiden, da XMLHttpRequest nur einen Wert über das load -Ereignis abruft (anstatt ein Promise selbst verfügbar zu machen). , aber die rekursive Natur der Funktion, die den Aufruf macht, sollte vertraut aussehen.

Nachdem ich vier Jahre mehr Erfahrung mit JavaScript hatte, als ich diese Frage ursprünglich geschrieben habe, habe ich den Code ein wenig aufgeräumt, aber es funktioniert im Wesentlichen auf die gleiche Weise.

// 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;
}

Eine kurze Erklärung von async/await

  • ein async function ist syntaktischer Zucker für ein Promise
  • await ist syntaktischer Zucker für Promise.then()
  • return innerhalb eines async function ist syntaktischer Zucker für resolve()
  • throw innerhalb eines async function ist syntaktischer Zucker für reject()

Wenn ein async function - Aufruf entweder einen anderen async function - Aufruf oder einen Promise zurückgibt, wird die Funktion/das Versprechen vor dem ursprünglichen Aufruf aufgelöst, genauso wie das Auflösen eines Promise würde im Promise Muster.

Sie können getRedirectUrl(someUrl).then(...).catch(...) also genauso aufrufen wie die ursprüngliche Frage.

Es ist wahrscheinlich zu beachten, dass die Verwendung eines XHR zum Auflösen einer umgeleiteten URL für jede URL fehlschlägt, die nicht den richtigen CORS-Header enthält.


Als zusätzlichen Bonus macht async/await einen iterativen Ansatz trivial.

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

Hier ist die vereinfachte Lösung:

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'));
0
cuddlemeister