Recursive Promise in javascript

前端 未结 6 1660
没有蜡笔的小新
没有蜡笔的小新 2020-12-08 01:58

I\'m writing a Javascript Promise that finds the final redirect URL of a link.

What I\'m doing is making a HEAD request in a Promise<

相关标签:
6条回答
  • 2020-12-08 02:39

    The problem is that the promise you return from getRedirectUrl() needs to include the entire chain of logic to get to the URL. You're just returning a promise for the very first request. The .then() you're using in the midst of your function isn't doing anything.

    To fix this:

    Create a promise that resolves to redirectUrl for a redirect, or null otherwise:

    function getRedirectsTo(xhr) {
        if (xhr.status < 400 && xhr.status >= 300) {
            return xhr.getResponseHeader("Location");
        }
        if (xhr.responseURL && xhr.responseURL != url) {
            return xhr.responseURL;
        }
    
        return null;
    }
    
    var p = new Promise(function (resolve) {
        var xhr = new XMLHttpRequest();
    
        xhr.onload = function () {
            resolve(getRedirectsTo(xhr));
        };
    
        xhr.open('HEAD', url, true);
        xhr.send();
    });
    

    Use .then() on that to return the recursive call, or not, as needed:

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

    Full solution:

    function getRedirectsTo(xhr) {
        if (xhr.status < 400 && xhr.status >= 300) {
            return xhr.getResponseHeader("Location");
        }
        if (xhr.responseURL && xhr.responseURL != url) {
            return xhr.responseURL;
        }
    
        return null;
    }
    
    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 () {
                resolve(getRedirectsTo(xhr));
            };
    
            xhr.open('HEAD', url, true);
            xhr.send();
        })
        .then(function (redirectsTo) {
            return redirectsTo
                ? getRedirectUrl(redirectsTo, redirectCount + 1)
                : url;
        });
    }
    
    0 讨论(0)
  • 2020-12-08 02:41

    If you're in an environment that supports async/await (virtually all modern environments do), you can write an async function that looks a bit more like a recursive function pattern we all know and love. It's not possible to completely avoid a Promise due to the nature of XMLHttpRequest only retrieving a value via the load event (rather than exposing a Promise itself), but the recursive nature of the function that makes the call should look familiar.

    Having four more years of JavaScript experience than I had when I originally wrote this question, I cleaned up the code a bit, but it works essentially the same way.

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

    A brief explanation of async/await

    • an async function is syntactic sugar for a Promise
    • await is syntactic sugar for Promise.then()
    • return within an async function is syntactic sugar for resolve()
    • throw within an async function is syntactic sugar for reject()

    If an async function returns either another async function call or a Promise, the function/promise will resolve before the original call resolves, exactly the same way that resolving a Promise would in the Promise pattern.

    So, you can call getRedirectUrl(someUrl).then(...).catch(...) exactly the same way the original question would have.

    It should probably be noted that using an XHR to resolve a redirected URL will fail for any URL that doesn't include the proper CORS header.


    As an added bonus, async/await makes an iterative approach 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');
    }
    
    0 讨论(0)
  • 2020-12-08 02:47

    Here's the simplified solution:

    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 讨论(0)
  • 2020-12-08 02:50

    Please check below example for Recursive Promise in javascript/typescript, It will not resolve the promise until number incremented to greater then 13.

    below code is suitable for typescript and modify it slightly for javascript.

    async iterate(number: number): Promise<any> {
            return new Promise((resolve, reject) => {
                let number = 0;
                if (number > 13) {
                    // recursive terminate condition
                    resolve(number);
                    return;
                } else {
                    number = number + 1;
                    // recursive call
                    this.iterate(number).then(resolve);
                }
    
            });
        }
    
    
    
    
    this.iterate().then((resolvedData: any) => {
               // wait until number is not greater than 13
               console.log(resolvedData);
        });
    
    0 讨论(0)
  • 2020-12-08 02:52

    Please check below example it will return factorial of a given number as we did in many programming languages.

    I have implemented below example using JavaScript promises.

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

    0 讨论(0)
  • 2020-12-08 02:54

    The following has two functions:

    • _getRedirectUrl - which is a setTimeout object simulation for looking up a single step lookup of a redirected URL (this is equivalent to a single instance of your XMLHttpRequest HEAD request)
    • getRedirectUrl - which is recursive calls Promises to lookup the redirect URL

    The secret sauce is the sub Promise whose's successful completion will trigger a call to resolve() from the parent promise.

    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>

    As another illustration of recursive Promises, I used it to solve a maze. The Solve() function is invoked recursively to advance one step in a solution to a maze, else it backtracks when it encounters a dead end. The setTimeout function is used to set the animation of the solution to 100ms per frame (i.e. 10hz frame rate).

    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>

    0 讨论(0)
提交回复
热议问题