Скасуйте ланцюжок обіцянок ECMAScript 6 для ванілі


110

Чи існує спосіб очищення .thens Promiseекземпляра JavaScript ?

Я написав тестову рамку JavaScript поверх QUnit . Рамка запускає тести синхронно, запускаючи кожен з а Promise. (Вибачте за довжину цього блоку коду. Я прокоментував це якнайкраще, тому він відчуває себе менш стомлюючим.)

/* Promise extension -- used for easily making an async step with a
       timeout without the Promise knowing anything about the function 
       it's 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()s моєї 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()вдалося її усунути.


Можливо зробити скасування з ES6 обіцянками, але це не властивість обіцянки (скоріше - це властивість функції, яка повертає її). Я можу зробити короткий приклад, якщо вам це цікаво.
Бенджамін Груенбаум

@BenjaminGruenbaum Я знаю, що минув майже рік, але мені все одно цікаво, чи є у вас час написати приклад. :)
dx_over_dt

1
Це було рік тому, але це було офіційно обговорено за два дні до вчора, коли жетони для скасування та відмінні обіцянки переходили на етап 1
Бенджамін Груенбаум,

3
Відповідь ES6 на скасування обіцянки помітна. Більше про це можна прочитати тут: github.com/Reactive-Extensions/RxJS
Frank Goortani

Пов’язання моєї відповіді щодо використання Prexбібліотеки для скасування обіцянок.
носраціо

Відповіді:


75

Чи існує спосіб очищення .thens екземпляра JavaScript Promise?

Ні, принаймні, не в ECMAScript 6. Обіцянки (та їх thenобробники) за замовчуванням не можна скасувати (на жаль) . Ес-дискусія (наприклад, тут ) трохи дискутує про те, як це зробити правильним способом, але який би підхід не виграв, це не приземлиться в ES6.

Поточна точка зору полягає в тому, що підкласифікація дозволить створювати відмінні обіцянки, використовуючи власну реалізацію (не впевнений, наскільки добре це буде працювати) .

До тих пір, поки мовний комітет не з'ясував найкращий спосіб (сподіваємось ES7?), Ви все ще можете використовувати реалізовані програми для обіцянки користувача, багато з яких скасовують функцію.

Поточна дискусія знаходиться у проектах https://github.com/domenic/cancelable-promise та https://github.com/bergus/promise-cancellation .


2
"Трохи дискусії" - я можу посилання на, можливо, 30 ниток на есдіскусі або GitHub :) (не кажучи вже про власну допомогу щодо скасування в bluebird 3.0)
Бенджамін Груенбаум

@BenjaminGruenbaum: Чи готові ви десь поділитися цими посиланнями? Я давно хотів узагальнити думки та спроби та викласти пропозицію на есдискус, тому буду радий, якщо зможу перевірити, що я нічого не забув.
Бергі

У мене вони зручні на роботі - значить, я буду їх через 3-4 дні. Ви можете перевірити специфікацію скасування обіцянок під обіцянками-аплусом для гарного початку.
Бенджамін Груенбаум

1
@ LUH3417: "нормальні" функції просто нудні в цьому плані. Ви запускаєте програму і чекаєте, поки вона закінчиться - або ви killїї і ігноруєте, в якому, можливо, дивному стані побічні ефекти залишили ваше середовище (тому ви, як правило, просто викидаєте це, наприклад, будь-які напівфабрикати). Неблокуючі або асинхронні функції, однак, побудовані для роботи в інтерактивних додатках, де ви хочете мати такий тонкий контроль над виконанням поточних операцій.
Бергі

6
Доменік видалив пропозицію TC39 ... ... cc @BenjaminGruenbaum
Серхіо

50

Хоча в 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/ismount-antipattern.html


1
це визначення скасованого просто відкидає обіцянку. це залежить від визначення "скасовано".
Олександр Міллс

1
А що станеться, якщо ви хочете скасувати набір обіцянок?
Матьє Брючер

1
Проблема такого підходу полягає в тому, що якщо у вас є Обіцянка, яка ніколи не вирішить або відхилить, вона ніколи не буде скасована.
DaNeSh

2
Це частково правильно, але якщо ви довго обіцяєте ланцюжок, такий підхід не буде працювати.
Вейко Карсико

11

Я дуже здивований, що ніхто не згадує 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 });

3
Я не вірю, що це працює. Якщо ви зміните обіцянку ввійти до журналу, запущення cancel()все одно призведе до виклику журналу. `` `const фактичнийPromise = новий Promise ((вирішити, відхилити) => {setTimeout (() => {console.log ('фактично викликаний'); resolution ()}, 10000)}); `` `
shmck

2
Питання полягало в тому, як скасувати обіцянку (=> зупинити ланцюжок then, яку потрібно виконати), а не як скасувати setTimeout(=> clearTimeout) або синхронний код, де, якщо ви не введете "if" після кожного рядка ( if (canceled) return), цього неможливо досягти. (Не робіть цього)
Pho3nixHun

10
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();

5

Дійсно зупинити виконання обіцянки неможливо, але ви можете викрасти відхилення та викликати його від самої обіцянки.

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('Messed up!'));
}, 1000);

1
@dx_over_dt Ваша редакція була б чудовим коментарем, але не зміною. Будь ласка, залиште такі суттєві редакції в межах компетенції ОП (якщо, звичайно, публікація не позначена як Wiki Wiki).
TylerH

@TylerH, тож сенс редагування виправляти помилки друку тощо? Або оновлювати інформацію, коли вона застаріла? Я новачок у можливості редагувати привілей інших публікацій.
dx_over_dt

@dx_over_dt Так, редагування полягає в поліпшенні публікацій, виправляючи помилки друку, граматичні помилки та додаючи підсвічування синтаксису (якщо хтось просто публікує купу коду, але не відступає або не позначає його на прикладі `` `). Додавання змістовного змісту, як-от додаткові пояснення чи міркування / обґрунтування речей, зазвичай є метою людини, яка опублікувала відповідь. Ви можете запропонувати це в коментарях, і ОП буде повідомлено про коментар, а потім зможе відповісти на нього, або вони можуть просто включити вашу пропозицію до публікації.
TylerH

@dx_over_dt Винятки становлять, якщо публікація позначена "Wiki Wiki", що вказує на те, що вона призначена для публікації у співпраці (наприклад, як Wikipedia), або якщо в публікації є серйозні проблеми, такі як груба / жорстока мова, небезпечний / шкідливий вміст ( наприклад, пропозиції або код, який може дати вам вірус або заарештувати тощо), або особисту інформацію, наприклад, записи про стан здоров'я, телефонні номери, кредитні картки тощо; сміливо видаляйте їх самі.
TylerH

Варто відзначити, що причина виконання не може бути зупинена в рамках обіцянки в тому, що JavaScript є однопоточним. Поки виконується функція обіцянки, більше нічого не виконується, тому немає нічого, що може викликати зупинку виконання.
dx_over_dt


2

Ось наша реалізація 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дзвінка
  • Покладайтеся на те, щоб скасування було відхилено замість рішення, на відміну від будь-якої іншої пропозиції чи реалізації

Витяги та коментарі вітаються


2

Обіцянку можна скасувати за допомогою AbortController.

Чи є метод очищення тоді: так, ви можете відхилити обіцянку AbortControllerоб'єктом, і тоді promiseобхід обійде всі блоки та перейде безпосередньо до блоку лову.

Приклад:

import "abortcontroller-polyfill";

let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")

let example = (signal) => {
    return new Promise((resolve, reject) => {
        let timeout = setTimeout(() => {
            elem.textContent = "Promise resolved";
            resolve("resolved")
        }, 2000);

        signal.addEventListener('abort', () => {
            elem.textContent = "Promise rejected";
            clearInterval(timeout);
            reject("Promise aborted")
        });
    });
}

function cancelPromise() {
    controller.abort()
    console.log(controller);
}

example(signal)
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log("Catch: ", error)
    });

document.getElementById('abort-btn').addEventListener('click', cancelPromise);

Html


    <button type="button" id="abort-btn" onclick="abort()">Abort</button>
    <div id="status"> </div>

Примітка: потрібно додати polyfill, підтримується не у всіх браузерах.

Живий приклад

Редагувати елегантне озеро-5jnh3


1

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

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

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();} 
 })
}

розчин для обгортки (фабричний)

Я знайшов рішення - передати об'єкт 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')
})

1

Спробуйте відмовитись від обіцянок : 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();
});

1

Якщо ваш код розміщений у класі, ви можете використовувати для цього декоратор. У вас є такий декоратор в утилітах-декораторів ( npm install --save utils-decorators). Він скасує попереднє виклик оформленого методу, якщо до вирішення попереднього виклику був зроблений ще один виклик цього конкретного методу.

import {cancelPrevious} from 'utils-decorators';

class SomeService {

   @cancelPrevious()
   doSomeAsync(): Promise<any> {
    ....
   }
}

https://github.com/vlio20/utils-decorators#cancelprevious-method


0

Якщо ви хочете зупинити виконання всіх тенів / виловів, ви можете зробити це, ввівши обіцянку, яка ніколи не вирішиться. Ймовірно, є попередження про витоку пам'яті, але це виправить проблему і не повинно викликати занадто багато марної пам'яті в більшості програм.

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

0

Встановіть властивість "скасовано" на сигналі "Обіцяння" then()та catch()рано вийти. Це дуже ефективно, особливо у веб-робітників, у яких наявні мікрозадачі в черзі в Обіцяннях від 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;
}


0

@Michael Yagudaev відповідь працює для мене.

Але оригінальна відповідь не пов'язує завершені обіцянки з .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();

0

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


0

Я все ще працюю над цією ідеєю, але ось як я реалізував скасовуване Обіцяння setTimeout як приклад.

Ідея полягає в тому, що обіцянку вирішують або відхиляють, коли б ви вирішили це зробити, тому слід вирішити, коли ви хочете скасувати, задовольнивши критерій, а потім самому викликати reject()функцію.

  • По-перше, я думаю, що є дві причини закінчити обіцянку достроково: її виконати і виконати (яку я назвав вирішити ) та скасувати (яку я назвав відхилити ). Звичайно, це лише моє відчуття. Звичайно, існує Promise.resolve()метод, але він знаходиться в самому конструкторі і повертає манекени, вирішені обіцянками. Цей resolve()метод екземпляра фактично розв’язує об'єкт, який створюється в даний час.

  • По-друге, ви можете радісно додати все що завгодно до новоствореного об’єкта обіцянки перед тим, як повернути його, і тому я щойно додав resolve()і reject()методи зробити його самостійним.

  • По-третє, хитрість полягає в тому, щоб пізніше отримати доступ до виконавця resolveта rejectфункцій, тому я просто зберігав їх у простому об'єкті зсередини закриття.

Я думаю, що рішення просте, і я не бачу великих проблем із цим.

function wait(delay) {
  var promise;
  var timeOut;
  var executor={};
  promise=new Promise(function(resolve,reject) {
    console.log(`Started`);
    executor={resolve,reject};  //  Store the resolve and reject methods
    timeOut=setTimeout(function(){
      console.log(`Timed Out`);
      resolve();
    },delay);
  });
  //  Implement your own resolve methods,
  //  then access the stored methods
      promise.reject=function() {
        console.log(`Cancelled`);
        clearTimeout(timeOut);
        executor.reject();
      };
      promise.resolve=function() {
        console.log(`Finished`);
        clearTimeout(timeOut);
        executor.resolve();
      };
  return promise;
}

var promise;
document.querySelector('button#start').onclick=()=>{
  promise=wait(5000);
  promise
  .then(()=>console.log('I have finished'))
  .catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.