Necesito crear una promesa de JavaScript que no se resolverá hasta que se cumpla una condición específica. Digamos que tengo una biblioteca de terceros y necesito esperar hasta que exista una determinada condición de datos dentro de esa biblioteca.
El escenario que me interesa es uno en el que no hay forma de saber cuándo se cumple esta condición más que mediante un simple sondeo.
Puedo crear una promesa que espera, y este código funciona, pero ¿hay un enfoque mejor o más conciso para este problema?
function ensureFooIsSet() { return new Promise(function (resolve, reject) { waitForFoo(resolve); }); } function waitForFoo(resolve) { if (!lib.foo) { setTimeout(waitForFoo.bind(this, resolve), 30); } else { resolve(); } }Uso:
ensureFooIsSet().then(function(){ ... });Normalmente implementaría un tiempo máximo de sondeo, pero no quería que eso empañara el problema aquí.
Una pequeña variación sería usar un IIFE con nombre para que su código sea un poco más conciso y evite contaminar el alcance externo:
function ensureFooIsSet() { return new Promise(function (resolve, reject) { (function waitForFoo(){ if (lib.foo) return resolve(); setTimeout(waitForFoo, 30); })(); }); }Aquí hay una función waitFor que uso bastante. Le pasa una función, y verifica y espera hasta que la función devuelva un valor verdadero, o hasta que se agote el tiempo de espera.
let sleep = ms => new Promise(r => setTimeout(r, ms)); let waitFor = async function waitFor(f){ while(!f()) await sleep(1000); return f(); };Ejemplos de usos:
let bed = await waitFor(() => document.getElementById('bedId')) if(!bed) doSomeErrorHandling(); await waitFor(() => el.loaded) await waitFor(() => video.currentTime > 21) await waitFor(() => video.currentTime > 21, 60*1000) if(await waitFor(someTest)) console.log('test passed') else console.log("test didn't pass after 20 seconds")Versión completa:
Esta versión se ocupa de más casos que la versión simple, nulo, indefinido, matriz vacía, etc., tiene un tiempo de espera, se puede pasar una frecuencia como argumento y registra en la consola lo que está haciendo con algunos colores agradables.
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));} /** * Waits for the test function to return a truthy value * example usage: * wait for an element to exist, then save it to a variable * let el = await waitFor(() => document.querySelector('#el_id'))) * timeout_ms and frequency are optional parameters */ async function waitFor(test, timeout_ms = 20 * 1000, frequency = 200) { if (typeof (test) != "function") throw new Error("test should be a function in waitFor(test, [timeout_ms], [frequency])") if (typeof (timeout_ms) != "number") throw new Error("timeout argument should be a number in waitFor(test, [timeout_ms], [frequency])"); if (typeof (frequency) != "number") throw new Error("frequency argument should be a number in waitFor(test, [timeout_ms], [frequency])"); let logPassed = () => console.log('Passed: ', test); let logTimedout = () => console.log('%c' + 'Timeout : ' + test, 'color:#cc2900'); let last = Date.now(); let logWaiting = () => { if(Date.now() - last > 1000) { last = Date.now(); console.log('%c' + 'waiting for: ' + test, 'color:#809fff'); } } let endTime = Date.now() + timeout_ms; let isNotTruthy = (val) => val === undefined || val === false || val === null || val.length === 0; // for non arrays, length is undefined, so != 0 let result = test(); while (isNotTruthy(result)) { if (Date.now() > endTime) { logTimedout(); return false; } logWaiting(); await sleep(frequency); result = test(); } logPassed(); return result; }¿Hay un enfoque más conciso para este problema?
Bueno, con esa función waitForFoo no necesitas una función anónima en tu constructor:
function ensureFooIsSet() { return new Promise(waitForFoo); } Para evitar contaminar el alcance, recomendaría envolver ambos en un IIFE o mover la función waitForFoo dentro del alcance de ensureFooIsSet :
function ensureFooIsSet(timeout) { var start = Date.now(); return new Promise(waitForFoo); function waitForFoo(resolve, reject) { if (window.lib && window.lib.foo) resolve(window.lib.foo); else if (timeout && (Date.now() - start) >= timeout) reject(new Error("timeout")); else setTimeout(waitForFoo.bind(this, resolve, reject), 30); } } Alternativamente, para evitar el enlace que se necesita para pasar la resolve y el reject , puede moverlo dentro de la devolución de llamada del constructor Promise como sugirió @DenysSéguret.
¿Hay un mejor enfoque?
Como comentó @BenjaminGruenbaum, puede ver que se asigne la propiedad .foo , por ejemplo, usando un setter:
function waitFor(obj, prop, timeout, expected) { if (!obj) return Promise.reject(new TypeError("waitFor expects an object")); if (!expected) expected = Boolean; var value = obj[prop]; if (expected(value)) return Promise.resolve(value); return new Promise(function(resolve, reject) { if (timeout) timeout = setTimeout(function() { Object.defineProperty(obj, prop, {value: value, writable:true}); reject(new Error("waitFor timed out")); }, timeout); Object.defineProperty(obj, prop, { enumerable: true, configurable: true, get: function() { return value; }, set: function(v) { if (expected(v)) { if (timeout) cancelTimeout(timeout); Object.defineProperty(obj, prop, {value: v, writable:true}); resolve(v); } else { value = v; } } }); }); // could be shortened a bit using "native" .finally and .timeout Promise methods } Puede usarlo como waitFor(lib, "foo", 5000) .