Используйте Promise, чтобы подождать, пока условие опроса не будет удовлетворено

Мне нужно создать обещание JavaScript, которое не будет разрешено до тех пор, пока не будет выполнено определенное условие. Допустим, у меня есть сторонняя библиотека, и мне нужно подождать, пока в этой библиотеке не появится определенное условие данных.

Меня интересует сценарий, в котором нет способа узнать, когда выполняется это условие, кроме простого опроса.

Я могу создать обещание, которое ждет его - и этот код работает, но есть ли лучший или более краткий подход к этой проблеме?

function ensureFooIsSet() {
    return new Promise(function (resolve, reject) {
        waitForFoo(resolve);
    });
}

function waitForFoo(resolve) {
    if (!lib.foo) {
        setTimeout(waitForFoo.bind(this, resolve), 30);
    } else {
        resolve();
    }
}

Использование:

ensureFooIsSet().then(function(){
    ...
});

Я бы обычно реализовывал максимальное время опроса, но не хотел, чтобы это скрывало проблему здесь.

Ответ 1

Небольшая вариация заключалась бы в использовании именованного IIFE, чтобы ваш код был немного более кратким и позволял избежать загрязнения внешней области:

function ensureFooIsSet() {
    return new Promise(function (resolve, reject) {
        (function waitForFoo(){
            if (lib.foo) return resolve();
            setTimeout(waitForFoo, 30);
        })();
    });
}

Ответ 2

Есть ли более краткий подход к этой проблеме?

Ну, с этой функцией waitForFoo вам вообще не нужна анонимная функция в вашем конструкторе:

function ensureFooIsSet() {
    return new Promise(waitForFoo);
}

Чтобы избежать загрязнения области, я бы рекомендовал либо обернуть оба в IIFE, либо переместить функцию waitForFoo внутри области 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);
    }
}

В качестве альтернативы, чтобы избежать привязки, необходимой для прохождения resolve и reject, вы можете переместить ее внутри обратного вызова конструктора Promise, например, предлагаемого @DenysSéguret.

Есть ли лучший подход?

Как прокомментировал @BenjaminGruenbaum, вы можете посмотреть свойство .foo, которое будет назначено, например. используя сеттер:

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
}

Вы можете использовать его как waitFor(lib, "foo", 5000).

Ответ 3

Здесь служебная функция, использующая async/await и обещания ES6 по умолчанию. promiseFunction - это асинхронная функция (или просто функция, которая возвращает обещание), которая возвращает истинное значение, если требование выполнено (пример ниже).

const promisePoll = (promiseFunction, { pollIntervalMs = 2000 } = {}) => {
  const startPoll = async resolve => {
    const startTime = new Date()
    const result = await promiseFunction()

    if (result) return resolve()

    const timeUntilNext = Math.max(pollIntervalMs - (new Date() - startTime), 0)
    setTimeout(() => startPoll(resolve), timeUntilNext)
  }

  return new Promise(startPoll)
}

Пример использования:

// async function which returns truthy if done
const checkIfOrderDoneAsync = async (orderID) => {
  const order = await axios.get('/order/${orderID}')
  return order.isDone
}

// can also use a sync function if you return a resolved promise
const checkIfOrderDoneSync = order => {
  return Promise.resolve(order.isDone)
}

const doStuff = () => {
  await promisePoll(() => checkIfOrderDone(orderID))
  // will wait until the poll result is truthy before
  // continuing to execute code
  somethingElse()
}

Ответ 4

function getReportURL(reportID) {
  return () => viewReportsStatus(reportID)
  .then(res => JSON.parse(res.body).d.url);
}

function pollForUrl(pollFnThatReturnsAPromise, target) {
  if (target) return P.resolve(target);
  return pollFnThatReturnsAPromise().then(someOrNone => pollForUrl(pollFnThatReturnsAPromise, someOrNone));
}

pollForUrl(getReportURL(id), null);

Ответ 5

Здесь функция waitFor, которой я пользуюсь совсем немного. Вы передаете ему функцию и запускаете ее, пока функция не вернет истинное значение или пока не истечет время ожидания. Примеры использования:

// wait for an element to exist, then assign it to a variable 
let bed = await waitFor(()=>document.getElementById('_1vd7r9f'))
if(!bed) doSomeErrorHandling();
// wait for a variable to be truthy
await waitFor(()=>el.loaded)
// wait for some test to be true
await waitFor(()=>video.currentTime>21)
// add a specific timeout
await waitFor(()=>video.currentTime>21, 60*1000)
// send an element as an argument once it exists
doSomething(await waitFor(()=>selector('...'))
// pass it some other test function
if(await waitFor(someTest)) console.log('test passed')
else console.log("test didn't pass after 20 seconds")

Это код для него

function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));    }
/* Waits for test function to return a truthy value
example usage:
    // wait for an element to exist, then save it to a variable
    var el = await waitFor(()=>$('#el_id')))                 // second timeout argument optional, or defaults to 20 seconds
 */
async function waitFor(test, timeout_ms=20*1000){
    return new Promise(async(resolve,reject)=>{
        if( typeof(timeout_ms) != "number") reject("Timeout argument not a number in waitFor(selector, timeout_ms)");
        var freq = 100;
        var result
        // wait until the result is truthy, or timeout
        while( result === undefined || result === false || result === null || result.length === 0 ){  // for non arrays, length is undefined, so != 0
            if( timeout_ms % 1000 <freq)        console.log('%c'+'waiting for: '+ test,'color:#809fff' );
            if( (timeout_ms -= freq) < 0 ){     console.log('%c'+'Timeout : '   + test,'color:#cc2900' );
                resolve(false);
                return;
            }
            await sleep(freq);
            result = typeof(test) === 'string' ? eval(test) : test();       // run the test and update result variable
        }
        // return result if test passed
        console.log('Passed: ', test);
        resolve(result);
    });
}