rogerxu / rogerxu.github.io

Roger Xu's Blog
3 stars 2 forks source link

Promise #77

Open rogerxu opened 7 years ago

rogerxu commented 7 years ago

JavaScript Promises: an Introduction  |  Web  |  Google Developers

Promise - JavaScript | MDN

Using promises - JavaScript | MDN

JavaScript Promise迷你书(中文版) · 看云

rogerxu commented 7 years ago

Articles

rogerxu commented 7 years ago

Promise

Master the JavaScript Interview: What is a Promise? – JavaScript Scene – Medium

History

  1. Node error-first callbacks
  2. Dojo Deferred API => Promises/A specification
  3. jQuery Deferred API
  4. Q, When, Bluebird => Promises/A+ specification
  5. ES6 Promise

How Promises Work

3 states

Promise Chaining

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;

Assuming each of the functions, fetch(), process(), and save() return promises, process() will wait for fetch() to complete before starting, and save() will wait for process() to complete before starting. handleErrors() will only run if any of the previous promises reject.

const wait = time => new Promise(
  res => setTimeout(() => res(), time)
);

wait(200)
  // onFulfilled() can return a new promise, `x`
  .then(() => new Promise(res => res('foo')))
  // the next promise will assume the state of `x`
  .then(a => a)
  // Above we returned the unwrapped value of `x`
  // so `.then()` above returns a fulfilled promise
  // with that value:
  .then(b => console.log(b)) // 'foo'
  // Note that `null` is a valid promise value:
  .then(() => null)
  .then(c => console.log(c)) // null
  // The following error is not reported yet:
  .then(() => {throw new Error('foo');})
  // Instead, the returned promise is rejected
  // with the error as the reason:
  .then(
    // Nothing is logged here due to the error above:
    d => console.log(`d: ${ d }`),
    // Now we handle the error (rejection reason)
    e => console.log(e)) // [Error: foo]
  // With the previous exception handled, we can continue:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // The following doesn't log. e was already handled,
  // so this handler doesn't get called:
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // When a promise is rejected, success handlers get skipped.
  // Nothing logs here because of the 'bar' exception:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;

Error Handling

Recommend ending all promise chains with a .catch().

save()
  .then(
    handleSuccess,
    handleNetworkError
  )
  .catch(handleProgrammerError)
;

Cancel a Promise

Common Mistakes

Abstracting Promise Cancellation

  1. Reject the cancel promise by default — we don’t want to cancel or throw errors if no cancel promise gets passed in.
  2. Remember to perform cleanup when you reject for cancellations.
  3. Remember that the onCancel cleanup might itself throw an error, and that error will need handling, too. (Note that error handling is omitted in the wait example above — it’s easy to forget!)
// HOF Wraps the native Promise API
// to add take a shouldCancel promise and add
// an onCancel() callback.
const speculation = (
  fn,
  cancel = Promise.reject() // Don't cancel by default
) => new Promise((resolve, reject) => {
  const noop = () => {};

  const onCancel = (
    handleCancel
  ) => cancel.then(
      handleCancel,
      // Ignore expected cancel rejections:
      noop
    )
    // handle onCancel errors
    .catch(e => reject(e))
  ;

  fn(resolve, reject, onCancel);
});

Use

const wait = (
  time,
  cancel = Promise.reject() // By default, don't cancel
) => speculation((resolve, reject, onCancel) => {
  const timer = setTimeout(resolve, time);

  // Use onCancel to clean up any lingering resources
  // and then call reject(). You can pass a custom reason.
  onCancel(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  });
}, cancel); // remember to pass in cancel!

wait(200, wait(500)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // 'Hello!'

wait(200, wait(50)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // [Error: Cancelled]
rogerxu commented 6 years ago

Promises: All The Wrong Ways

Promises: All The Wrong Ways | getiblog - 众成翻译

False Start

// anti-pattern
foo()
  .then(nextStep);

// good
Promise.resolve(foo())
  .then(nextStep);

Start Delays

Promise.resolve()
  .then(firstStep);

// good
firstStep();

Failing To Start

// bad
new Promise(function(resolve, reject) {
  firstStep()
    .then(resolve, reject);
});

If firstStep() results in an exception, it will automatically be caught by the Promise machinery, and turned into a promise rejection that the rest of the chain can articulate handling for.

The async function normalizes whatever comes back from the firstStep() function call into a promise, including catching any exception and turning it into a promise rejection.

async () => firstStep()()
  .then();

Deconstruction

Essentially, this approach treates a promise like a single-value event conduit. One part of the app holds the “write end” of the pipe, the deferred. Another part of the app holds the “read end”, the promise.

But it’s an anti-pattern and that you should try to avoid it. At a minimum, if you have to do so, you should be hiding that kind of trick inside the plumbing of a library, not exposing the deferred and promise separately at the top level of logic in your application.

function makePromise() {
  var def = {};
  var pr = new Promise(function init(resolve,reject) {
    def.resolve = resolve;
    def.reject = reject;
  });

  return [pr, def];
}

var [pr, def] = makePromise();

Abstractions For Value Communication

We can use already built-in parts of JavaScript to create a pull abstraction directly. A generator can create a producer iterator:

function *setupProducer() {
  var pr = new Promise(function(resolve, reject){
    setupEvent(resolve, reject);
  });

  yield pr;
}

var producer = setupProducer();

// Here’s how we later pull a value:
var pr = producer.next();

pr.then(function(value) {
  // ..
});

We yield a promise for that single next value that the producer will eventually produce. So the consumer is pulling (iterating) out each push instance (promise).

Promise Side-Effects

// bad
Promise.resolve().then(function(){
  console.log("one");
});

Promise.resolve().then(function(){
  console.log( "two" );
});

Construction Sequencing

// bad
var fn;

var pr = new Promise(function init(resolve){
  fn = resolve;
});

fn(42);

The above snippet is equivalent to just:

var pr = Promise.resolve(42);

Scopes

// bad, side-effect programming
function getOrderDetails(orderID) {
  var _order;

  return db.find("orders", orderID)
    .then(function(order) {
      _order = order;
      return db.find("customers", order.customerID )
    })
    .then(function(customer) {
      _order.customer = customer;
      return _order;
    });
}
function getOrderDetails(orderID) {
  return db.find("orders", orderID)
    .then(function(order) {
      return db.find("customers", order.customerID)
        // nested `then()` instead of flattened to outer chain,
        // so that we still have lexical scope access to `order`
        .then(function(customer) {
          order.customer = customer;
          return order;
        });
    });
}

Normally, nesting promise then(..)s is a bad idea, and even goes by the name “promise hell”.

But in these cases where multiple values need to be available (in scope), a flattened vertical promise chain necessitates the worser evil of side-effects. Promise nesting is the better choice.

Promise Chain

// bad
firstStep()
  .then(secondStep)
  .then(thirdStep);

Use the synchronous-async pattern.

async function main() {
  await firstStep();
  await secondStep();
  await thirdStep();
}

async function main() {
  try {
    var val1 = await firstStep();
    var val2 = await secondStep(val1);
    var val3 = await thirdStep(val1, val2);

    console.log("Final: ", val3);
  } catch (err) {
    console.error(err);
  }
}

For the most part, we should try not to ever call then(..) explicitly. As much as possible, that should stay hidden as an implementation detail.

Calling then(..) on a promise is a code smell and anti-pattern.

rogerxu commented 6 years ago

Callback Hell

Avoiding Callback Hell in Node.js

rogerxu commented 6 years ago

Promise Hell

How to escape Promise Hell – Ronald Chen – Medium

Nested .then() with linearly dependent Promises

Bad

fetchBook()
  .then((book) => {
    return formatBook(book)
      .then((book) => {
        return sendBookToPrinter(book);
      });
  });

Good

fetchBook()
  .then(formatBook)
  .then(sendBookToPrinter);

Nested .then() with independent Promises

Bad

demandMoreDonuts()
  .then(() => {
    return greet('fred')
      .then(() => {
        return closeGate();
      });
  });

Good - No order

Promise.all([
  demandMoreDonuts(),
  greet('fred'),
  closeGate(),
]);

Good - serialize the order

Promise.resolve()
  .then(demandMoreDonuts)
  .then(() => greet('fred'))
  .then(closeGate);
]);

Nested .then() with multiple dependent Promises

Bad

connectDatabase()
  .then((database) => {
    return findAllBooks(database)
      .then((books) => {
        return getCurrentUser(database)
          .then((user) => {
            return pickTopRecommentations(books, user);
          });
      });
  });

Good

const databasePromise = connectDatabase();

const booksPromise = databasePromise.then(findAllBooks);

const userPromise = databasePromise.then(getCurrentUser);

Promise.all([
  booksPromise,
  userPromise,
]).then(([books, user]) => pickTopRecommentations(books, user));
rogerxu commented 4 years ago

Deferred

You're Missing the Point of Promises

jQuery Promise

deferred.promise() | jQuery API Documentation - Return a Deferred's Promise object.

三個不要再用 jQuery Promise 的理由 | Pymaster

jQuery.Deferred().reject()
  .then(null, function () {
    return 'error handled';
  })
  .then(function (res) {
    log('jquery resolve', res);
  }, function (res) {
    log('jquery reject', res);
  });

promise chain 中被 reject,经过 rejection handler 后

Coming from jQuery · kriskowal/q Wiki

转换 jQuery Promise 的最简单方法就是直接用 Promise 把它包起来 (resolve):

Promise.resolve(jqPromise).then(...)