javascript 中的递归 Promise

Recursive Promise in javascript

我正在写一个 Javascript Promise 来找到 link.

的最终重定向 URL

我正在做的是使用 XMLHttpRequestPromise 中发出 HEAD 请求。然后,在加载时,检查 300 范围内的某些东西的 HTTP 状态,或者它是否有一个 responseURL 附加到对象并且 url 与它是单手的不同。

如果这两个都不成立,我resolve(url)。否则,我在响应 URL 和 resolve().

上递归调用 getRedirectUrl()

这是我的代码:

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(...) 但这永远不会解决。

我的猜测是我正在使用的模式(我基本上是临时想到的)完全不正确。

问题是 return 来自 getRedirectUrl() 的承诺需要包含整个逻辑链才能到达 URL。您只是 return 对第一个请求的承诺。您在函数中使用的 .then() 没有执行任何操作。

解决这个问题:

创建一个解析为 redirectUrl 重定向的承诺,否则 null

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

that 上使用 .then() 到 return 递归调用,或不使用,视需要而定:

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

完整解决方案:

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

请查看下面的示例,它将 return factorial 给定的数字,就像我们在许多编程语言中所做的那样。

我已经使用 JavaScript 承诺实现了以下示例。

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

这是简化的解决方案:

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

如果您处于支持 async/await 的环境中(几乎所有现代环境都支持),您可以编写一个看起来更像递归的 async function我们都知道和喜爱的函数模式。由于 XMLHttpRequest 仅通过 load 事件检索值(而不是暴露 Promise 本身)的性质,不可能完全避免 Promise,但是递归进行调用的函数的性质应该看起来很熟悉。

比我最初写这个问题时多了四年的 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 functionPromise
  • 的语法糖
  • awaitPromise.then()
  • 的语法糖
  • returnasync function 中是 resolve()
  • 的语法糖 async function 中的
  • throwreject()
  • 的语法糖

如果 async function returns 另一个 async function 调用或 Promise,function/promise 将在原始调用解析之前解析,完全相同在 Promise 模式中解析 Promise 的方式。

因此,您可以调用 getRedirectUrl(someUrl).then(...).catch(...) 与原始问题完全相同的方式。

可能应该注意的是,使用 XHR 解析重定向的 URL 对于任何不包含正确 CORS header.[=41= 的 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');
}

另一个注意事项:现代浏览器有一个 fetch() 函数,它的作用与 createXHRPromise() 的作用基本相同,但用途更广。它在节点中不受支持,但有一个名为 node-fetch.

的 npm 包

下面有两个函数:

  • _getRedirectUrl - 这是一个 setTimeout 对象模拟,用于查找重定向的单步查找 URL(这相当于您的 XMLHttpRequest HEAD 请求的单个实例)
  • getRedirectUrl - 这是递归调用承诺查找重定向 URL

秘诀是子 Promise,其成功完成将触发父 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>

作为递归 Promises 的另一个例子,我用它来解决迷宫问题。 Solve() 函数被递归调用以在迷宫的解决方案中前进一步,否则在遇到死胡同时回溯。 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>

请检查下面的示例以了解 javascript/typescript 中的递归 Promise,直到数字递增到大于 13 时才会解析 Promise。

下面的代码适用于 typescript 并稍微修改一下 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);
    });

如果您有带异步调用的嵌套数组结构,此解决方案(基于以前的答案)可能会有所帮助。该示例为它在(可能)嵌套数组中找到的每个值运行一个 setTimeout() 并在完成所有这些值时解析:

const recursiveCall = (obj) => {
    return new Promise((resolve) => {
        if(obj instanceof Array){
            let cnt = obj.length;
            obj.forEach(el => {
                recursiveCall(el)
                .then(() => {
                    if(!--cnt)return resolve();
                })
                
            });
        } else {
            setTimeout(() => {
                console.log(obj);
                return resolve();
            }, obj);
            
        }
    })
}

recursiveCall([100,50,[10,[200, 300],30],1]).then(() => console.log('done'));

>1
>10
>30
>50
>100
>200
>300
>done