Как отменить свое обещание?

JavaScript Программа перевода самородков Promise

Как отменить свое обещание?

В версии ES6 ECMAScript, международного стандарта языка JavaScript, были представлены новые асинхронные нативные объекты.Promise. Это очень мощная концепция, позволяющая избежать пресловутоголовушка обратного вызова. Например, несколько асинхронных операций можно легко записать следующим образом:

function updateUser(cb) {
  fetchData(function(error, data) => {
    if (error) {
      throw error;
    }
    updateUserData(data, function(error, data) => {
      if (error) {
        throw error;
      }
      updateUserAddress(data, function(error, data) => {
        if (error) {
          throw error;
        }
        updateMarketingData(data, function(error, data) => {
          if (error) {
            throw error;
          }

          // finally!
          cb();
        });
      });
    });
  });
}

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

// 我们不再需要回调函数了 – 只需要使用 then 方法
// 处理函数的返回结果
function updateUser() {
  return fetchData()
    .then(updateUserData)
    .then(updateUserAddress)
    .then(updateMarketingData);
}

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

Одним из недостатков использования цепочки промисов является то, что у нас нет доступа к области действия каждой функции обратного вызова (или переменным, которые не возвращаются), вы можете прочитать это доктором Алексом Раушмайером.a great articleДля решения этой проблемы.

Однако я нашелЭта проблема, вы не можете отменить промис, что является критической проблемой. Иногда вынеобходимостьОтмените обещания, и вы создадите обходные пути — объем работы зависит от того, как часто вы используете эту функцию.

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

BluebirdЭто библиотека реализации Promise, полностью совместимая с нативными объектами Promise и добавляющая несколько полезных методов к объекту-прототипу Promise.prototype (Примечание переводчика: расширение методов нативных объектов Promise). Здесь мы только вводимcancelметод, который частично достигает того, что мы хотим — когда мы используемpromise.cancelПри отмене промисов это позволяет нам иметь кастомную логику (зачем частичная реализация? Потому что код многословный и не универсальный).

В нашем примере давайте посмотрим, как использовать Bluebird для реализации отмены обещания:

import Promise from 'Bluebird';

function updateUser() {
  return new Promise((resolve, reject, onCancel) => {
    let cancelled = false;

    // 你需要更改 Bluebird 的配置,才能使用 cancellation 特性
    // http://bluebirdjs.com/docs/api/promise.config.html
    onCancel(() => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    });

    return fetchData()
      .then(wrapWithCancel(updateUserData))
      .then(wrapWithCancel(updateUserAddress))
      .then(wrapWithCancel(updateMarketingData))
      .then(resolve)
      .catch(reject);

    function wrapWithCancel(fn) {
      // promise resolved 的状态只需要传递一个参数
      return (data) => {
        if (!cancelled) {
          return fn(data);
        }
      };
    }
  });
}

const promise = updateUser();
// 等一会...
promise.cancel(); // 用户还是会被更新

Как видите, мы добавили много кода в предыдущий чистый пример. К сожалению, другого пути нет, так как мы не можем остановить выполнение случайной цепочки промисов (если хотим, то должны обернуть ее в другую функцию), поэтому нам нужно обернуть каждую callback-функцию функцией, обрабатывающей отмененное состояние .

Чистые обещания

Описанная выше техника не так специфична для Bluebird, она больше связана с интерфейсом — вы можете реализовать свою версию отмены, но с дополнительными свойствами/переменными. Обычно этот метод называютcancellationToken, по сути, он почти такой же, как и предыдущий, но не вPromise.prototype.cancelС помощью этого метода мы создаем его экземпляр на другом объекте — мы можем использоватьcancelСвойство возвращает объект, или мы можем принять дополнительный параметр, объект, куда мы добавим свойство.

function updateUser() {
  let resolve, reject, cancelled;
  const promise = new Promise((resolveFromPromise, rejectFromPromise) => {
    resolve = resolveFromPromise;
    reject = rejectFromPromise;
  });

  fetchData()
    .then(wrapWithCancel(updateUserData))
    .then(wrapWithCancel(updateUserAddress))
    .then(wrapWithCancel(updateMarketingData))
    .then(resolve)
    .then(reject);

  return {
    promise,
    cancel: () => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    }
  };

  function wrapWithCancel(fn) {
    return (data) => {
      if (!cancelled) {
        return fn(data);
      }
    };
  }
}

const { promise, cancel } = updateUser();
// 等一会...
cancel(); // 用户还是会被更新

Это немного более подробное решение, чем предыдущее, но оно решает ту же проблему и является жизнеспособным решением, если вы не используете Bluebird (или не хотите использовать нестандартный подход с промисами). Как видите, мы изменили сигнатуру — теперь мы возвращаем объект вместо промиса, но на самом деле мы можем передать в функцию объектный параметр и дописатьcancelметод (или экземпляр Promise с обезьяньим патчем, но это также вызовет у вас проблемы позже). Это хорошее решение, если у вас есть это требование только в нескольких местах.

переключиться на генераторы

Генераторы — еще одна новая функция ES6, но по какой-то причине они не получили широкого распространения. Подумайте, прежде чем использовать его — новички в вашей команде не поймут его или все участники будут чувствовать себя непринужденно? Кроме того, он существует на некоторых других языках, таких какPython, поэтому это решение должно быть легко использовать в команде.

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

// 这是运行我们异步代码的核心方法
// 并且提供 cancellation 方法
function runWithCancel(fn, ...args) {
  const gen = fn(...args);
  let cancelled, cancel;
  const promise = new Promise((resolve, promiseReject) => {
    // 定义 cancel 方法,并返回它
    cancel = () => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    };

    let value;

    onFulfilled();

    function onFulfilled(res) {
      if (!cancelled) {
        let result;
        try {
          result = gen.next(res);
        } catch (e) {
          return reject(e);
        }
        next(result);
        return null;
      }
    }

    function onRejected(err) {
      var result;
      try {
        result = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }
      // 假设我们总是接收 Promise,所以不需要检查类型
      return value.then(onFulfilled, onRejected);
    }
  });

  return { promise, cancel };
}

Это довольно длинная функция, но в основном она (кроме проверок, которые, конечно, очень рудиментарная реализация) — сам код останется точно таким же, и мы будем воспринимать буквальноcancelметод! Давайте посмотрим, как это использовать в нашем примере:

// * 表示这是一个 Generator 函数
// 你可以把 * 放到几乎任何地方 :)
// 这种写法语法上和 async/await 很相似
function* updateUser() {
  // 假设我们所有的函数都返回 Promise
  // 否则需要调整我们的执行器函数
  // 去接受 Generator
  const data = yield fetchData();
  const userData = yield updateUserData(data);
  const userAddress = yield updateUserAddress(userData);
  const marketingData = yield updateMarketingData(userAddress);
  return marketingData;
}

const { promise, cancel } = runWithCancel(updateUser);

// 见证奇迹的时刻
cancel();

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

Генератор, я думаю, самый масштабируемый вариант, потому что вы можете буквально делать все, что хотите — если что-то случится, вы можете приостановить, подождать, повторить попытку или запустить другой Генератор. Тем не менее, я не очень часто вижу их в коде JavaScript, поэтому вам следует подумать о внедрении и когнитивной нагрузке — у вас действительно много вариантов его использования? Если да, то это очень хорошее решение, и вы можете поблагодарить себя в будущем.

Обратите внимание на async/await

существуетES2017версия предоставляет async/await, что вы можете сделать в Node.js (Версия 7.6после) используйте их без каких-либо флагов. К сожалению, ничто не поддерживает отмену промиса, а поскольку асинхронные функции неявно возвращают промис, мы не можем его пощупать (прикрепить свойство или вернуть что-то еще), только значение разрешенного/отклоненного состояния. Это означает, что для того, чтобы нашу функцию можно было отменить, нам нужно передать объект и обернуть каждый вызов в наш знаменитый метод-оболочку:

async function updateUser(token) {
  let cancelled = false;

  // 我们不调用 reject,因为我们无法访问
  // 返回的 Promise
  // 我们不调用其它函数
  // 在结束时调用 reject
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // 因为我们已经包装了所有的函数,以防取消
  // 不需要调用任何实际函数来达到这一点
  // 我们也不能调用 reject 方法
  // 因为我们无法控制返回的 Promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// 等一会...
token.cancel(); // 用户还是会被更新

Это очень похожее решение, но поскольку мы неcancelвызывает метод reject, поэтому это может запутать читателя. С другой стороны, сейчас это стандартная фича языка, с очень удобным синтаксисом, позволяющим использовать результат предыдущего вызова позже (поэтому здесь решается проблема связывания промисов), и имеет очень лаконичный и интуитивно понятный способ пройтиtry / catchобработка ошибок. Итак, если отмена вас больше не беспокоит (или вы можете что-то отменить таким образом), то эта функция определенно является лучшим способом написания асинхронного кода в современном JavaScript.

Используйте потоки (например, RxJS)

Потоки — это совершенно другое понятие, но на самом деле оно более широко используется.Не только в JavaScript, так что вы можете думать об этом как о независимом от платформы режиме. Потоки могут быть лучше или хуже, чем Promie/Generator. Если вы прикоснулись к нему и использовали его для обработки некоторой (или всей) асинхронной логики, вы обнаружите, что потоки лучше, если нет, вы найдете потоки хуже, потому что это совершенно другой подход.

Я не эксперт по использованию потоков, просто использовал несколько, и я думаю, что вы должны использовать их для всех асинхронных событий или не использовать их вообще. Итак, если вы уже используете их, этот вопрос не должен быть для вас слишком сложным, поскольку это давно известная функция библиотеки Streams.

Как я уже упоминал, у меня недостаточно опыта работы с Streams, чтобы предоставить решение с их использованием, поэтому я просто поставлю пару ссылок об отмене реализации Streams:

принимать

Дела идут в правильном направлении - будет добавлен выборкаabortметод, как отменить обещание, будет горячо обсуждаться в течение долгого времени в будущем. Может ли отмена обещания работать? Может быть, может быть, нет. Кроме того, отмена промиса не критична для многих приложений — да, вы можете сделать несколько дополнительных запросов, но очень редко получается более одного результата запроса. Кроме того, если это происходит один или два раза, вы можете использовать расширенный пример для решения этих конкретных функций с самого начала. Однако, если в вашем приложении много таких случаев, рассмотрите перечисленные выше.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.