【问题标题】:Recursive Promise in javascriptjavascript中的递归承诺
【发布时间】:2015-05-15 06:08:10
【问题描述】:

我正在编写一个 Javascript Promise 来查找链接的最终重定向 URL。

我正在做的是使用XMLHttpRequestPromise 中发出HEAD 请求。然后,在加载时,检查 300 范围内的某些内容的 HTTP 状态,或者它是否有一个 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(...) 但这永远不会解决。

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

【问题讨论】:

  • p.then(...) 的功能不会产生任何可观察到的副作用,returns 没有任何意义。
  • 作为一般规则,尽量避免在 Promise 构造函数中做太多工作。这很少是您想要的,并且通过then() 和多个函数的常规链,问题变得更加清晰。

标签: javascript recursion promise


【解决方案1】:

问题是您从getRedirectUrl() 返回的承诺需要包含整个逻辑链才能到达 URL。您只是为第一个请求返回了一个承诺。您在函数中使用的 .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 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;
    });
}

【讨论】:

  • 我很困惑!我看不出这里发生了什么。如果我调用 getRedirectUrl() 它会返回一个 promise,并且该 promise 有一个 resolve() 函数可以做一些事情。当这些事情完成后,可能会递归遵循重定向。但!第一个 xhr.open(); 怎么做如果 xhr.send() 唯一出现的地方是在 Promise resolve() 函数中,它会发生吗?我对 Promise 和 JavaScript 还很陌生,所以这里可能会遗漏一些东西。
  • @theWebalyst 带有xhr.open()xhr.send() 的函数在getRedirectUrl() 返回之前被调用。传入Promise 构造函数的函数会立即被Promise 构造函数调用。
  • @theWebalyst 这里的微妙细节是resolve 函数实际上是传递给Promise 构造函数的处理函数的参数。您的想法中导致混淆的错误是 promise 有一个 resolve() 函数可以做一些事情。。实际上,处理函数做了一些事情,并在完成时调用它传递的resolve 函数。您可以添加第二个名为 reject 的参数,并在重定向过多时调用它而不是 throwing。效果是一样的。
  • 这很聪明,非常需要建议
  • 5 年后完全救了我。谢谢!
【解决方案2】:

请查看下面的示例,它将返回给定数字的 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);

【讨论】:

  • 为我工作。
【解决方案3】:

这是简化的解决方案:

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

【讨论】:

  • 如果索引足够大,您将遇到“超出最大调用堆栈大小”错误。
  • @balazs 好了,你可以把这个return resolve(recursiveCall(++index)) 改成return setTimeout(() =&gt; resolve(recursiveCall(++index)), 0); 就可以了。但这与问题没有直接关系,在简化示例中可能会造成混淆
  • 当然,对于问题的用例,它不会达到这个限制,但对于那些想要更通用解决方案的人(问题的标题对我来说听起来很笼统)我认为这很好知道限制。
  • @balazs 完全同意
【解决方案4】:

如果您处于支持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() 的语法糖
  • return 中的 async functionresolve() 的语法糖
  • throwasync function 中是reject() 的语法糖

如果async function 返回另一个async function 调用或Promise,则函数/promise 将在原始调用解析之前解析,与PromisePromise 模式中解析的方式完全相同.

因此,您可以像原来的问题一样拨打getRedirectUrl(someUrl).then(...).catch(...)

应该注意的是,对于任何不包含正确 CORS 标头的 URL,使用 XHR 解析重定向 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不支持,但是有一个npm包叫node-fetch

【讨论】:

    【解决方案5】:

    下面有两个功能:

    • _getRedirectUrl - 这是一个 setTimeout 对象模拟,用于查找重定向 URL 的单步查找(这相当于您的 XMLHttpRequest HEAD 请求的单个实例)
    • getRedirectUrl - 递归调用 Promises 以查找重定向 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>

    【讨论】:

      【解决方案6】:

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

      以下代码适用于打字稿,对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);
          });
      

      【讨论】:

        【解决方案7】:

        如果您有一个带有异步调用的嵌套数组结构,这个解决方案(基于以前的答案)可能会有所帮助。该示例为它在(可能)嵌套数组中找到的每个值运行 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
        

        【讨论】:

        • 根据您的代码,我猜您对 JavaScript 还很陌生?如果您有兴趣,我使用声明式函数式编程而不是命令式过程式编程编写了一些“JavaScript 编写方式”的注释示例。 codepen.io/dfoverdx/pen/bGreoXK?editors=1111
        猜你喜欢
        • 1970-01-01
        • 2017-12-18
        • 1970-01-01
        • 2014-02-04
        • 1970-01-01
        • 2015-12-05
        • 1970-01-01
        • 2018-05-29
        • 1970-01-01
        相关资源
        最近更新 更多