Рекурсивное обещание в javascript

Я пишу Javascript Promise, который находит окончательный URL-адрес переадресации ссылки.

Я делаю запрос HEAD в Promise с помощью XMLHttpRequest. Затем, при загрузке, проверьте статус HTTP для чего-то в диапазоне 300 или если у него есть responseURL, прикрепленный к объекту, и этот URL-адрес отличается от того, что он был одним.

Если ни одно из них не верно, я resolve(url). В противном случае я рекурсивно вызываю getRedirectUrl() по URL-адресу ответа и 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) { ... });

Проблема в том, что resolve(); в getRedirectUrl вызовет then() из вызывающей функции до того, как он вызовет рекурсивный вызов getRedirectUrl, и в этот момент URL-адрес undefined.

Я пытался, а не p.then(...getRedirectUrl...) делать return self.getRedirectUrl(...), но это никогда не будет разрешено.

Моя догадка заключается в том, что шаблон, который я использую (который я в основном придумал на лету), не прав, вообще.

Ответы

Ответ 1

Проблема в том, что обещание, которое вы возвращаете из 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();
});

Используйте .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;
    });
}

Ответ 2

Вот упрощенное решение:

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

Ответ 3

Пожалуйста, проверьте ниже пример, он вернет 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);

Ответ 4

Если вы находитесь в среде, которая поддерживает async/await (практически во всех современных средах), вы можете написать async function которая выглядит немного больше как шаблон рекурсивной функции, которую мы все знаем и любим. Невозможно полностью избежать Promise из-за природы XMLHttpRequest только извлекающего значение через событие load (а не представляющего само 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 function является синтаксическим сахаром для Promise
  • await синтаксического сахара для Promise.then()
  • return в async function является синтаксическим сахаром для resolve()
  • throw в async function является синтаксическим сахаром для reject()

Если async function возвращает либо другой вызов async function либо Promise, функция/обещание разрешается до разрешения исходного вызова, точно так же, как и при разрешении Promise в шаблоне Promise.

Таким образом, вы можете вызывать getRedirectUrl(someUrl).then(...).catch(...) точно так же, как в исходном вопросе.

Вероятно, следует отметить, что использование XHR для разрешения перенаправленного URL не удастся для любого URL, который не содержит правильный заголовок CORS.


В качестве дополнительного бонуса, 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');
}

Ответ 5

Следующее имеет две функции:

  • _getRedirectUrl - имитация объекта setTimeout для поиска одношагового поиска перенаправленного URL-адреса (это эквивалентно одному экземпляру вашего запроса HEAD XMLHttpRequest)
  • getRedirectUrl - это рекурсивные вызовы Обещает найти URL перенаправления

Секретным соусом является подчиненное обещание, успешное завершение которого вызовет функцию 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>