Подтвердить что ты не робот

Отмена ванильной цепочки обещаний ECMAScript 6

Есть ли способ очистки .then экземпляра JavaScript Promise?

Я написал тестовую среду JavaScript поверх QUnit. Структура запускает тесты синхронно, запустив каждый из них в Promise. (Извините за длину этого кодового блока. Я прокомментировал это как можно лучше, поэтому он чувствует себя менее утомительным.)

/* Promise extension -- used for easily making an async step with a
       timeout without the Promise knowing anything about the function 
       it waiting on */
$$.extend(Promise, {
    asyncTimeout: function (timeToLive, errorMessage) {
        var error = new Error(errorMessage || "Operation timed out.");
        var res, // resolve()
            rej, // reject()
            t,   // timeout instance
            rst, // reset timeout function
            p,   // the promise instance
            at;  // the returned asyncTimeout instance

        function createTimeout(reject, tempTtl) {
            return setTimeout(function () {
                // triggers a timeout event on the asyncTimeout object so that,
                // if we want, we can do stuff outside of a .catch() block
                // (may not be needed?)
                $$(at).trigger("timeout");

                reject(error);
            }, tempTtl || timeToLive);
        }

        p = new Promise(function (resolve, reject) {
            if (timeToLive != -1) {
                t = createTimeout(reject);

                // reset function -- allows a one-time timeout different
                //    from the one original specified
                rst = function (tempTtl) {
                    clearTimeout(t);
                    t = createTimeout(reject, tempTtl);
                }
            } else {
                // timeToLive = -1 -- allow this promise to run indefinitely
                // used while debugging
                t = 0;
                rst = function () { return; };
            }

            res = function () {
                clearTimeout(t);
                resolve();
            };

            rej = reject;
        });

        return at = {
            promise: p,
            resolve: res,
            reject: rej,
            reset: rst,
            timeout: t
        };
    }
});

/* framework module members... */

test: function (name, fn, options) {
    var mod = this; // local reference to framework module since promises
                    // run code under the window object

    var defaultOptions = {
        // default max running time is 5 seconds
        timeout: 5000
    }

    options = $$.extend({}, defaultOptions, options);

    // remove timeout when debugging is enabled
    options.timeout = mod.debugging ? -1 : options.timeout;

    // call to QUnit.test()
    test(name, function (assert) {
        // tell QUnit this is an async test so it doesn't run other tests
        // until done() is called
        var done = assert.async();
        return new Promise(function (resolve, reject) {
            console.log("Beginning: " + name);

            var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
            $$(at).one("timeout", function () {
                // assert.fail() is just an extension I made that literally calls
                // assert.ok(false, msg);
                assert.fail("Test timed out");
            });

            // run test function
            var result = fn.call(mod, assert, at.reset);

            // if the test returns a Promise, resolve it before resolving the test promise
            if (result && result.constructor === Promise) {
                // catch unhandled errors thrown by the test so future tests will run
                result.catch(function (error) {
                    var msg = "Unhandled error occurred."
                    if (error) {
                        msg = error.message + "\n" + error.stack;
                    }

                    assert.fail(msg);
                }).then(function () {
                    // resolve the timeout Promise
                    at.resolve();
                    resolve();
                });
            } else {
                // if test does not return a Promise, simply clear the timeout
                // and resolve our test Promise
                at.resolve();
                resolve();
            }
        }).then(function () {
            // tell QUnit that the test is over so that it can clean up and start the next test
            done();
            console.log("Ending: " + name);
        });
    });
}

Если тест истечет, мой тайм-аут обещания будет assert.fail() в тесте, чтобы тест был отмечен как неудачный, что все хорошо и хорошо, но тест продолжает работать, потому что тест Promise (result) все еще ждет его решения.

Мне нужен хороший способ отменить мой тест. Я могу это сделать, создав поле в фрейм-модуле this.cancelTest или что-то подобное, и проверяя каждую так часто (например, в начале каждой итерации then()) в тесте, следует ли отменять. Однако в идеале я мог бы использовать $$(at).on("timeout", /* something here */), чтобы очистить оставшуюся then() от моей переменной result, так что ни один из оставшихся тестов не будет запущен.

Есть ли что-то подобное?

Быстрое обновление

Я попытался использовать Promise.race([result, at.promise]). Это не сработало.

Обновление 2 + путаница

Чтобы разблокировать меня, я добавил несколько строк с mod.cancelTest/опросом в тесте. (Я также удалил триггер события.)

return new Promise(function (resolve, reject) {
    console.log("Beginning: " + name);

    var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
    at.promise.catch(function () {
        // end the test if it times out
        mod.cancelTest = true;
        assert.fail("Test timed out");
        resolve();
    });

    // ...

}).then(function () {
    // tell QUnit that the test is over so that it can clean up and start the next test
    done();
    console.log("Ending: " + name);
});

Я установил точку останова в выражении catch и ударил. Теперь меня смущает то, что оператор then() не вызывается. Идеи?

Обновление 3

Подумал последнее. fn.call() выбрасывает ошибку, которую я не поймал, поэтому обещание теста отклонялось до того, как at.promise.catch() смог его решить.

4b9b3361

Ответ 1

Есть ли способ очистки .then экземпляра Java Promise?

Нет. Не по крайней мере в ECMAScript 6. Promises (и их обработчики then) по умолчанию (к сожалению) не поддаются вычислению. Существует немного обсуждения es-discuss (например, здесь) о том, как это сделать правильно, но какой бы ни был способ выиграть его Приземлиться в ES6.

Текущая точка зрения заключается в том, что подклассификация позволит создать отмену Promises с использованием вашей собственной реализации (не уверен, насколько хорошо это будет работать).

Пока коммьюнити языка не выяснит лучший способ (надеюсь, ES7?), вы все равно можете использовать пользовательские реализации Promise, многие из которых отменяют функцию.

Текущее обсуждение находится в https://github.com/domenic/cancelable-promise и https://github.com/bergus/promise-cancellation черновики.

Ответ 2

Хотя в ES6 нет стандартного способа сделать это, для обработки этого файла существует библиотека под названием Bluebird.

Существует также рекомендуемый способ, описанный как часть документации для реагирования. Он похож на то, что у вас есть в ваших 2 и 3-х обновлениях.

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise

Взято из: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

Ответ 3

Я действительно удивлен, что никто не упоминает Promise.race в качестве кандидата на это:

const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) });
let cancel;
const cancelPromise = new Promise((resolve, reject) => {
    cancel = reject.bind(null, { canceled: true })
})

const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel });

Ответ 4

const makeCancelable = promise => {
    let rejectFn;

    const wrappedPromise = new Promise((resolve, reject) => {
        rejectFn = reject;

        Promise.resolve(promise)
            .then(resolve)
            .catch(reject);
    });

    wrappedPromise.cancel = () => {
        rejectFn({ canceled: true });
    };

    return wrappedPromise;
};

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

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();

Ответ 6

простая версия:

просто выдайте функцию отклонения.

function Sleep(ms,cancel_holder) {

 return new Promise(function(resolve,reject){
  var done=false; 
  var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms);
  cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();} 
 })
}

решение wraper (factory)

найденное решение - передать объект cancel_holder. он будет иметь функцию отмены. если он имеет функцию отмены, то он отменяется.

Эта функция отмены отклоняет обещание с ошибкой ( "отменено" ).

Перед тем, как разрешить, отклонить или on_cancel предотвратить функцию отмены, вызывается без причины.

Я нашел удобным передать действие отмены инъекцией

function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) {
  if(!cancel_holder)cancel_holder={};
  return new Promise( function(resolve,reject) {
    var canceled=false;
    var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);}
    var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);}
    var on_cancel={}
    cancel_holder.cancel=function(){
      if(canceled) return; canceled=true;

      delete cancel_holder.cancel;
      cancel_holder.canceled=true;

      if(on_cancel.cancel)on_cancel.cancel();
      if(optional_external_cancel)optional_external_cancel();

      reject(new Error('canceled'));
    };

    return promise_fn.call(this,resolve2,reject2,on_cancel);        
  });
}

function Sleep(ms,cancel_holder) {

 return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){

  var t=setTimeout(resolve, ms);
  oncacnel.cancel=function(){if(t)clearTimeout(t);}     

 })
}


let cancel_holder={};

// meanwhile in another place it can be canceled
setTimeout(function(){  if(cancel_holder.cancel)cancel_holder.cancel(); },500) 

Sleep(1000,cancel_holder).then(function() {
 console.log('sleept well');
}, function(e) {
 if(e.message!=='canceled') throw e;
 console.log('sleep interrupted')
})

Ответ 7

На самом деле невозможно остановить выполнение обещания, но вы можете угнать отклонение и вызвать его из самого обещания.

class CancelablePromise {
  constructor(executor) {
    let _reject = null;
    const cancelablePromise = new Promise((resolve, reject) => {
      _reject = reject;
      return executor(resolve, reject);
    });
    cancelablePromise.cancel = _reject;

    return cancelablePromise;
  }
}

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

const p = new CancelablePromise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolved!');
    resolve();
  }, 2000);
})

p.catch(console.log);

setTimeout(() => {
  p.cancel(new Error('Fucked up!'));
}, 1000);

Ответ 8

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

new Promise((resolve, reject) => {
    console.log('first chain link executed')
    resolve('daniel');
}).then(name => {
    console.log('second chain link executed')
    if (name === 'daniel') {
        // I don't want to continue the chain, return a new promise
        // that never calls its resolve function
        return new Promise((resolve, reject) => {
            console.log('unresolved promise executed')
        });
    }
}).then(() => console.log('last chain link executed'))

// VM492:2 first chain link executed
// VM492:5 second chain link executed
// VM492:8 unresolved promise executed

Ответ 9

Здесь наша реализация https://github.com/permettez-moi-de-construire/cancellable-promise

Используется как

const {
  cancellablePromise,
  CancelToken,
  CancelError
} = require('@permettezmoideconstruire/cancellable-promise')

const cancelToken = new CancelToken()

const initialPromise = SOMETHING_ASYNC()
const wrappedPromise = cancellablePromise(initialPromise, cancelToken)


// Somewhere, cancel the promise...
cancelToken.cancel()


//Then catch it
wrappedPromise
.then((res) => {
  //Actual, usual fulfill
})
.catch((err) => {
  if(err instanceOf CancelError) {
    //Handle cancel error
  }

  //Handle actual, usual error
})

который:

  • Не касается API Promise
  • Давайте сделаем дальнейшую отмену внутри catch call
  • Положитесь об отмене отвергается вместо разрешенных в отличии от любого другого предложения или реализаций

Тянет и комментарии приветствуются

Ответ 10

Установите свойство "отменено" на Promise, чтобы сигнализировать then() и catch() чтобы выйти рано. Это очень эффективно, особенно в Web Workers, которые существующие microtasks выстраивались в очереди в Promises из onmessage обработчиков.

// Queue task to resolve Promise after the end of this script
const promise = new Promise(resolve => setTimeout(resolve))

promise.then(_ => {
  if (promise.canceled) {
    log('Promise cancelled.  Exiting early...');
    return;
  }

  log('No cancelation signaled.  Continue...');
})

promise.canceled = true;

function log(msg) {
  document.body.innerHTML = msg;
}

Ответ 11

@Михаил Ягудаев ответ работает для меня.

Но оригинальный ответ не связывал завернутое обещание с .catch() для обработки отклонения, вот мое улучшение поверх ответа @Michael Yagudaev:

const makeCancelablePromise = promise => {
  let hasCanceled = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise
      .then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val)))
      .catch(
        error => (hasCanceled ? reject({ isCanceled: true }) : reject(error))
      );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled = true;
    }
  };
};

// Example Usage:
const cancelablePromise = makeCancelable(
  new Promise((rs, rj) => {
    /*do something*/
  })
);
cancelablePromise.promise.then(() => console.log('resolved')).catch(err => {
  if (err.isCanceled) {
    console.log('Wrapped promise canceled');
    return;
  }
  console.log('Promise was not canceled but rejected due to errors: ', err);
});
cancelablePromise.cancel();

Ответ 12

Если p - это переменная, содержащая Promise, то p.then(empty); должен отклонить обещание, когда оно в конце концов выполнится или если оно уже выполнено (да, я знаю, что это не оригинальный вопрос, но это мой вопрос). "empty" - это function empty() {}. Я только новичок и, вероятно, ошибаюсь, но эти другие ответы кажутся слишком сложными. Обещания должны быть простыми.

Ответ 13

let r = null;

let p = new Promise( (resolve,reject)=> {
  r = reject;
  setTimeout( resolve, 2000 );
});

console.log("start");

p
  .then ( ()=>console.log("end") )
  .catch( (e)=>console.log(e) );

setTimeout( ()=>{
  r("break");
}, 1000);

Ответ 14

Попробуйте обещание об отказе: https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable
import AbortablePromise from "promise-abortable";

const timeout = new AbortablePromise((resolve, reject, signal) => {
  setTimeout(reject, timeToLive, error);
  signal.onabort = resolve;
});

Promise.resolve(fn()).then(() => {
  timeout.abort();
});