Как отменить тайм-аут внутри Javascript Promise?

Я играю с промисами в JavaScript и пытался обещать функцию setTimeout:

function timeout(ms) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('timeout done');
    }, ms);
  }); 
}

var myPromise=timeout(3000); 

myPromise.then(function(result) { 
  console.log(result); // timeout done
})

Довольно просто, но мне было интересно, как я могу отменить тайм-аут до того, как обещание будет выполнено. timeout возвращает объект Promise, поэтому я теряю доступ к значению, которое возвращает setTimeout, и не могу отменить тайм-аут через clearTimeout. Как лучше всего это сделать?

Кстати, для этого нет реальной цели, мне просто интересно, как к этому подойти. Также я добавил его сюда http://plnkr.co/edit/NXFjs1dXWVFNEOeCV1BA?p=preview.


person spirytus    schedule 17.08.2014    source источник
comment
вы также можете использовать декораторы, подробнее здесь: stackoverflow.com/a/61242606/1691423   -  person vlio20    schedule 16.04.2020


Ответы (4)


Редактировать 2021 все платформы используют AbortController в качестве примитива отмены, и для этого есть встроенная поддержка.

В Node.js

// import { setTimeout } from 'timers/promises' // in ESM
const { setTimeout } = require('timers/promises');
const ac = new AbortController();

// cancellable timeout
(async () => {
  await setTimeout(1000, null, { signal: ac.signal });
})();

// abort the timeout, rejects with an ERR_ABORT
ac.abort();

В браузерах

Вы можете заполнить этот API и использовать то же, что и в примере выше:


function delay(ms, value, { signal } = {}) {
    return new Promise((resolve, reject) => {
        const listener = () => {
            clearTimeout(timer);
            reject(new Error('Aborted'));
        };
        const timer = setTimeout(() => {
            signal?.removeEventListener('abort', listener);
            resolve(value);
        }, ms);
        if (signal?.aborted) {
            listener();
        }
        signal?.addEventListener('abort', listener);
    });
}

Что вы можете сделать, вы можете вернуть отмену из вашей функции timeout и вызвать ее при необходимости. Таким образом, вам не нужно хранить timeoutid глобально (или во внешней области), а также это может управлять несколькими вызовами функции. Каждый экземпляр объекта, возвращаемый функцией timeout, будет иметь свой собственный отменитель, который может выполнять отмену.

function timeout(ms) {
  var timeout, promise;

  promise = new Promise(function(resolve, reject) {
    timeout = setTimeout(function() {
      resolve('timeout done');
    }, ms);
  }); 

  return {
           promise:promise, 
           cancel:function(){clearTimeout(timeout );} //return a canceller as well
         };
}

var timeOutObj =timeout(3000); 

timeOutObj.promise.then(function(result) { 
  console.log(result); // timeout done
});

//Cancel it.
timeOutObj.cancel();

Plnkr

person PSL    schedule 17.08.2014

Однако ответ PSL правильный - есть несколько предостережений, и я бы сделал это немного по-другому.

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

Здесь:

function timeout(ms, value) {
    var p = new Promise(function(resolve, reject) {
        p._timeout = setTimeout(function() {
            resolve(value);
        }, ms);
        p.cancel = function(err) {
            reject(err || new Error("Timeout"));
            clearTimeout(p._timeout); // We actually don't need to do this since we
                                      // rejected - but it's well mannered to do so
        };
    });
    return p;
}

Что позволило бы нам сделать:

var p = timeout(1500)
p.then(function(){
     console.log("This will never log");
})

p.catch(function(){
     console.log("This will get logged so we can now handle timeouts!")
})
p.cancel(Error("Timed out"));

Кто-то может быть заинтересован в полной отмене, и действительно, некоторые библиотеки поддерживают это напрямую как функцию библиотеки. На самом деле, я бы осмелился сказать, что большинство. Однако это вызывает проблемы с помехами. Цитирую KrisKowal из здесь:

Моя позиция по отмене изменилась. Теперь я убежден, что отмена (bg:, которая распространяется) по своей сути невозможна с абстракцией промисов, потому что промисы могут быть множественными зависимостями, а зависимые могут быть введены в любое время. Если какой-либо зависимый отменит обещание, он сможет помешать будущим зависимым. Есть два способа обойти проблему. Один из них — ввести отдельную «возможность» отмены, которая может быть передана в качестве аргумента. Другой заключается в том, чтобы ввести новую абстракцию, возможно, подходящую «Задачу», которая в обмен на требование, чтобы каждая задача имела только одного наблюдателя (один затем вызывается всегда), может быть отменена, не опасаясь вмешательства. Задачи будут поддерживать метод fork() для создания новой задачи, позволяя другому зависимому лицу сохранить задачу или отложить отмену.

person Benjamin Gruenbaum    schedule 17.08.2014
comment
Хотя я нигде не могу найти это задокументировано, функция поселенца Promise, похоже, запускается в том же ходе события, что и var p = new Promise(), поэтому у вас не может быть ссылок на p внутри поселенца. Решение (по крайней мере, единственное, которое я могу придумать) довольно уродливое, но работает - DEMO. - person Roamer-1888; 17.08.2014
comment
Таким образом исправлено, это лучшее решение. - person Roamer-1888; 17.08.2014
comment
В качестве дополнения к цитате kriskowal поделился некоторыми мыслями здесь - person Clark Pan; 11.09.2014
comment
У Бенджамина-Грюнбаума есть лучший ответ. Если вы добавите членов в обещание, вы не сможете отменить зависимое обещание (т. е. результат .then()); см. эту статью для получения дополнительной информации. - person arolson101; 19.06.2015
comment
Это дает TypeError: Cannot set property '_timeout' of undefined в функции тайм-аута. - person dd619; 02.05.2019

Приведенные выше ответы @Benjamin и @PSL работают, но что, если вам нужно, чтобы отменяемый тайм-аут использовался внешним источником при внутренней отмене?

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

// externally usage of timeout 
async function() {
  await timeout() // timeout promise 
} 

// internal handling of timeout 
timeout.cancel() 

Мне самому нужна была такая реализация, поэтому вот что я придумал:

/**
 * Cancelable Timer hack.
 *
 *  @notes
 *    - Super() does not have `this` context so we have to create the timer
 *      via a factory function and use closures for the cancelation data.
 *    - Methods outside the consctutor do not persist with the extended
 *      promise object so we have to declare them via `this`.
 *  @constructor Timer
 */
function createTimer(duration) {
  let timerId, endTimer
  class Timer extends Promise {
    constructor(duration) {
      // Promise Construction
      super(resolve => {
        endTimer = resolve
        timerId = setTimeout(endTimer, duration)
      })
      // Timer Cancelation
      this.isCanceled = false
      this.cancel = function() {
        endTimer()
        clearTimeout(timerId)
        this.isCanceled = true
      }
    }
  }
  return new Timer(duration)
}

Теперь вы можете использовать таймер следующим образом:

let timeout = createTimer(100)

И отмените обещание в другом месте:

 if (typeof promise !== 'undefined' && typeof promise.cancel === 'function') {
  timeout.cancel() 
}
person Lorenzo    schedule 29.03.2017

Это мой ответ на TypeScript:

  private sleep(ms) {
    let timerId, endTimer;
    class TimedPromise extends Promise<any> {
      isCanceled: boolean = false;
      cancel = () => {
        endTimer();
        clearTimeout(timerId);
        this.isCanceled = true;
      };
      constructor(fn) {
        super(fn);
      }
    }
    return new TimedPromise(resolve => {
      endTimer = resolve;
      timerId = setTimeout(endTimer, ms);
    });
  }

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

const wait = sleep(10*1000);
setTimeout(() => { wait.cancel() },5 * 1000);
await wait; 
person Richard    schedule 24.01.2018