Статьи

Обещания, которым можно доверять

Обещания JavaScript обеспечивают надежную модель программирования для будущего развития JavaScript.

Так что здесь я играю с обещаниями.

Сначала мне нужно немного файла package.json:

    {
      "name": "promises",
      "scripts": {
        "test": "node node_modules/mocha/bin/mocha"
      },
      "devDependencies": {
        "chai": "^1.10.0",
        "mocha": "^2.0.1"
      },
      "dependencies": {
        "q": "^1.1.2"
      }
    }

Теперь я могу написать свой первый тест (test / promises_test.js):

var Q = require('q');
var expect = require('chai').expect;

describe('promises', function() {

  it('can be resolved', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve();
    });

    promise.then(function() {
      done();
    });
  });
    
});

Обратите внимание, что функция «it» принимает параметр функции «done», чтобы гарантировать, что тест ожидает, пока обещание не будет выполнено. Удалите вызов функции done () или resolution (), и проверка завершится по таймауту.

  it('can pass resolution value', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(6*8);
    });

    promise.then(function(value) {
      expect(value).to.equal(42);
      done();
    });
  });

Этот тест не пройден, но из-за тайм-аута. Причина в том, что сделано никогда не называется. Давайте улучшим тест.

  it('can pass resolution value', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(6*8);
    });

    promise.done(function(value) {
      expect(value).to.equal(42);
      done();
    });
  });

Использование «done ()» вместо «then ()» означает, что цепочка обещаний завершена. Если мы не справились с ошибками, то готово вызовет исключение. Тест больше не истекает, но хорошо проваливается:

 promises
   V can be resolved
   1) can pass resolution value


 1 passing (11ms)
 1 failing

 1) promises can pass resolution value:

     Uncaught AssertionError: expected 48 to equal 42
     + expected - actual

     +42
     -48

     at C:\Users\jhannes\experiments\promises\test\promises_test.js:22:24

И мы можем это исправить:

  it('can pass resolution value', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(6*7);
    });

    promise.done(function(value) {
      expect(value).to.equal(42);
      done();
    });
  });

Урок: Всегда заканчивайте цепочку обещаний с помощью done ().

Чтобы разделить это, вы можете разделить then и Done:

  it('can pass resolution value', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(6*7);
    });

    promise.then(function(value) {
      expect(value).to.equal(42);
    }).done(done);
  });

Для этого есть еще один способ:

  it('can return the promise to mocha', function() {
    var promise = Q.Promise(function(resolve) {
      resolve(6*7);
    });

    return promise.then(function(value) {
      expect(value).to.equal(42);
    });
  });

Но что такое цепочка обещаний?

  it('can pass values in a promise chain', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(6);
    });

    promise.then(function(v) {
      return v*7;
    }).done(function(v) {
      expect(v).to.equal(42);
    }).done(done);
  });

Это очень круто, когда нам нужно несколько обещаний:

  it('can resolve multiple promises', function(done) {
    var promise = Q.Promise(function(resolve) {
      resolve(['abc', 'def', 'gh']);
    });

    promise.then(function(array) {
      return Q.all(array.map(function(v) {
        return Q.Promise(function(resolve) {
          resolve(v.length);
        })
      }));
    }).done(function(lengthArray) {
      expect(lengthArray).to.eql([3,3,2]);
    }).done(done);
  });

Обратите внимание, что done вызывается только тогда, когда ВСЕ строки рассчитаны (асинхронно).

Поначалу это может показаться странным, но чрезвычайно полезно при работе с графами объектов:

  var savePurchaseOrder = function(purchaseOrder) {
    return orderDao
      .save(purchaseOrder)
      .then(function(orderId) {
        purchaseOrder.orderLines.forEach(function(line) {
          line.orderId = orderId;
        });
        return Q.all(
          purchaseOrder.orderLines.map(orderLineDao.save));
      });
  };

Здесь методы сохранения dao.orderDao и orderLineDao оба возвращают обещания. Наша функция «savePurchaseOrder» также возвращает обещание, которое разрешается, когда все сохраняется. И все происходит асинхронно.

Хорошо, вернемся к основам об обещаниях.

  it('can reject a promise', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      reject('something went wrong');
    });

    promise.then(null, function(error) {
      expect(error).to.equal('something went wrong');
    }).done(done);
  });

Здесь вызывается вторая функция done (). Мы можем использовать «fail ()» в качестве ярлыка:

  it('can reject a promise', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      reject('something went wrong');
    });

    promise.fail(function(error) {
      expect(error).to.equal('something went wrong');
      done();
    }); // trailing promise!
  });

Но это не так хорошо. Если сравнение не удастся, этот тест истечет! Это лучше:

  it('can reject a promise', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      reject('something went wrong');
    });

    promise.fail(function(error) {
      expect(error).to.equal('something went wrong');
    }).done(done);
  });

Конечно, нам нужно обрабатывать и неожиданные события:

  it('treats errors as rejections', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      throw new Error('whoops');
    });

    promise.fail(function(error) {
      expect(error).to.eql(new Error('whoops'));
    }).done(done);
  });

И конечно: что-то может потерпеть неудачу в середине цепи:

  it('treats errors as rejections', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      resolve(null);
    });

    promise.then(function(value) {
      return value.length;
    }).fail(function(error) {
      expect(error.message).to.eql("Cannot read property 'length' of null");
    }).done(done);
  });

Ошибка автоматически распространяется на первый обработчик ошибки:

  it('propagates failure to the first failure handler', function(done) {
    var promise = Q.Promise(function(resolve, reject) {
      resolve(null);
    });

    promise.then(function(value) {
      return value.length; // Throws Error
    }).then(function(length) {
      this.test.error("never called");
    }).then(function(number) {
      this.test.error("never called");
    }).fail(function(error) {
      expect(error.message).to.eql("Cannot read property 'length' of null");
    }).done(done);    
  });

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

Вы можете найти весь исходный код здесь. Кроме того, обязательно ознакомьтесь со слайдами Скотта Сойета о функциональном JavaScript , чтобы узнать больше об обещаниях, карри и других вкусных функциональных вещах.

Спасибо моему бывшему коллеге и соратнику изгнанию Санату за вдохновение написать эту статью.