返回列表

转载:ES 5-6-7: From Callbacks to Promises to Generators to Async/await

默认分类 2016/12/02 03:23

原文地址: https://medium.com/@rdsubhas/es6-from-callbacks-to-promises-to-generators-87f1c0cd8f2e#.ob6888mul

ES 5-6-7: From Callbacks to Promises to Generators to Async/await

Let’s take a real life use case with the request library.

Step 1: Callback hell — N levels deep

This is how typical NodeJS code would look like. Every function receives a callback function with a commonly used signature: function(err, response) { }

var request = require('request');
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;

function foo(finalCallback) {
  request.get(url1, function(err1, res1) {
    if (err1) { return finalCallback(err1); }
    request.post(url2, function(err2, res2) {
      if (err2) { return finalCallback(err2); }
      request.put(url3, function(err3, res3) {
        if (err3) { return finalCallback(err3); }
        request.del(url4, function(err4, res4) {
          // let's stop here
          if (err4) { return finalCallback(err4); }
          finalCallback(null, "whew all done");
        })
      })
    })
  })
}
// use that function somewhere

Step 2: Promises — 1 level deep

Promise libraries take the typical callback function: function(err, response) { } And split those arguments into separate then/catch chainable callbacks: .then(function(response) { }).catch(function(err) { })

You can use Q, Bluebird or any of the innumerable promise libraries. I’ve used bluebird here. You have to first “promisify” the old-style callback library methods.

Note how you have to use “request.getAsync” instead of “request.get”. That’s called “promisification” (line #2) — it converts regular methods into promise-style methods. Also note how it simplifies the place where foo() is invoked as well.

Step 3: Promises+Generators — 0 level flat

Let’s combine the power of promises and ES6 generators.

“foo” is now almost sequential. We reduced the program from 27 lines (callback style) to 23 lines (promises) to 19 lines (promises + generators). It now appears completely flat without any nested functions. Of course, behind the scenes there are still callbacks happening, but WYSIWYG.

var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;

function foo() {
  return request.getAsync(url1)
  .then(function(res1) {
    return request.postAsync(url2);
  }).then(function(res2) {
    return request.putAsync(url3);
  }).then(function(res3) {
     return request.delAsync(url4);
  }).then(function(res4) {
     return "whew all done";
  });
}

// use that function somewhere
foo().then(function(message) {
  console.log("success!", message);
}).catch(function(err) {
  console.log("error!", err);
});

Note that the place where we call “foo” still uses promises. We can flatten that as well to use generators, and it becomes a simple try/catch.

function* callerFunction() {
  try {
    message = yield foo();
    console.log("success!", message);
  } catch (err) {
    console.log("error!", err);
  }
}
callerFunction = Promise.coroutine(callerFunction);
callerFunction();

And the place where you call “callerFunction” can also be flattened, and so on, and so on until the topmost entry point of the app. For web applications, that entry point is the web framework. If the web framework is aware of using generators and promises, you can basically make all your functions as generators and forever flat. And you get close to something like koa.

Transforming Tests

Generators are immensely useful in writing Tests (Mocha/etc).Tests usually have a lot of callbacks, but they run in sequence. What a waste of asynchronicity. You can write test cases today using Generators, without worrying about switching testing frameworks.

var Promise = require('bluebird');
var assert = require('assert');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;

describe('context', function() {

  it('should kill callbacks', Promise.coroutine(function*() {
    // Use destructuring since "request" returns multiple values
    [res1] = yield request.getAsync(url1);
    assert.equal(200, res1.statusCode);
    // Array destructuring might not work in some node versions
    // In that case, simply use res[0].statusCode

    [res2] = yield request.getAsync(url2);
    assert.equal(200, res2.statusCode);
    [res3] = yield request.getAsync(url3);
    assert.equal(200, res3.statusCode);
    [res4] = yield request.getAsync(url4);
    assert.equal(200, res4.statusCode);

    // Yay! No more "done()" and no more callbacks
  }));

});

Step 4: ES7 async/await

ES7 async/await further works on top of generators. Babel already has it in enabled (though its still in staging), so you can try this today.

var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;

async function foo() {
  var res1 = await request.getAsync(url1);
  var res2 = await request.getAsync(url2);
  var res3 = await request.getAsync(url3);
  var res4 = await request.getAsync(url4);
  return "whew all done";
}

// use that function somewhere
foo().then(function(message) {
  console.log("success!", message);
}).catch(function(err) {
  console.log("error!", err);
});

Wait, how does this work?

This whole magic simply works because NodeJS callbacks have a standard signature of function(err, response) { }.

Promise libraries simply act as a glue between your code, target function (request.get/post/…) and a Deferred object.

deferred = // create a Deferred() object
customCallback = function(err, response) {
  if (err) deferred.reject(err);
  else deferred.resolve(response);
}
// call original request.get/post/... with customCallback
// return you the Deferred's promise

When ES6 introduced generators, Promise libraries hooked in with a nify hack: If you “yield a promise”, you’ll get the resolved value back.

try {
    response          =   yield request.getAsync(...)
    ^^^^^^^^^^^^^^^^^^^^^       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
     to get the response            yield a promise
} catch (error) { }
^^^^^^^^^^^^^^^^^^^^^
        catch the error

How Generators work is a larger topic altogether. I recommend reading

davidwalsh.name/es6-generators or any of the innumerable articles on the web.