chaining async method calls - javascript

2020-02-11 23:26发布

问题:

You have a prototype object Foo with two async method calls, bar and baz.

var bob = new Foo()

Foo.prototype.bar = function land(callback) {
  setTimeout(function() {
    callback()
    console.log('bar');
  }, 3000);
};

Foo.prototype.baz = function land(callback) {
  setTimeout(function() {
    callback()
    console.log('baz');
  }, 3000);
};

We want to do bob.bar().baz() and have it log "bar" and "baz" sequentially.

If you cannot modify the method calls (including passing in your callback function), how can you pass a default callback into these method calls?

Some ideas:

  1. Wrap "bob" with decorator (still fuzzy on how to implement, could use a small example)

  2. Modify constructor to assign default callback if none assigned (have not considered if this is possible or not)

  3. Use a generator wrapper that will continue to call next method until none are left?

回答1:

The more recommended way instead is to use promises. As this is a community-wide trend to do async stuffs.

We want to do bob.bar().baz() and have it log "bar" and "baz" sequentially.

Why would you want to do that just to achieve this bob.bar().baz() "syntax"? When you could do it pretty simply using the Promise API without additional efforts to make that syntax work that indeed increases code complexity making the actual code difficult to understand.

So, you might want to consider using the promise-based approach somewhat like this, which does offer much more flexibility than what you would have achieved by your approach:

Foo.prototype.bar = function () {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve()
            console.log('bar');
        }, 3000);
    };
};

Foo.prototype.baz = function () {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve()
            console.log('baz');
        }, 3000);
    };
};

Now you'd do this to run them sequentially one after another:

var bob = new Foo();

bob.bar().then(function() {
   return bob.baz();
});

// If you're using ES2015+ you could even do:
bob.bar().then(() => bob.baz());

If you need to chain more functions you could simply do it:

bob.bar()
    .then(() => bob.baz())
    .then(() => bob.anotherBaz())
    .then(() => bob.somethingElse());  

Anyway, if you're not used to using promises you might want to read this



回答2:

Warning this isn't quite right yet. Ideally we'd subclass Promise and have proper then/catch functionality but there are some caveats with subclassing bluebird Promise. The idea is to store an internal array of promise generating functions, then when a Promise is waited on (then/await) serially wait on those promises.

const Promise = require('bluebird');

class Foo {
  constructor() {
    this.queue = [];
  }

  // promise generating function simply returns called pGen
  pFunc(i,pGen) {
    return pGen();
  }

  bar() {
    const _bar = () => {
      return new Promise( (resolve,reject) => {
        setTimeout( () => {
          console.log('bar',Date.now());
          resolve();
        },Math.random()*1000);
      })      
    }
    this.queue.push(_bar);
    return this;
  }

  baz() {
    const _baz = () => {
      return new Promise( (resolve,reject) => {
        setTimeout( () => {
          console.log('baz',Date.now());
          resolve();
        },Math.random()*1000);
      })      
    }
    this.queue.push(_baz);
    return this;
  }

  then(func) {
    return Promise.reduce(this.queue, this.pFunc, 0).then(func);
  }
}


const foo = new Foo();
foo.bar().baz().then( () => {
  console.log('done')
})

result:

messel@messels-MBP:~/Desktop/Dropbox/code/js/async-chain$ node index.js 
bar 1492082650917
baz 1492082651511
done


回答3:

If you want to avoid callback hell and keep your sanity ES6 promises are the most appropriate approach for the sake of functional programming. You just chain up your sequential asynchronous tasks in the asynchronous timeline just like working in a synchronous timeline.

In this particular case you just need to promisify your asynchronous functions. Assume that your asynch functions takes a data and a callback like asynch(data,myCallback). Let us assume that the callback is error first type.

Such as;

var myCallback = (error,result) => error ? doErrorAction(error)
                                         : doNormalAction(result)

When your asynch function is promisified, you will actually be returned a function which takes your data and returns a promise. You are expected to apply myCallback at the then stage. The return value of myCallback will then be passed to the next stage at where you can invoke another asynch function supplied with the return value of myCallback and this goes on and on as long as you need. So let's see how we shall implement this abstract to your workflow.

function Foo(){}

function promisify(fun){
  return (data) => new Promise((resolve,reject) => fun(data, (err,res) => err ? reject(err) : resolve(res)));
}

function myCallback(val) {
  console.log("hey..! i've got this:",val);
  return val;
}

var bob = new Foo();

Foo.prototype.bar = function land(value, callback) {
  setTimeout(function() {
    callback(false,value*2);  // no error returned but value doubled and supplied to callback
    console.log('bar');
  }, 1000);
};

Foo.prototype.baz = function land(value, callback) {
  setTimeout(function() {
    callback(false,value*2);  // no error returned but value doubled and supplied to callback
    console.log('baz');
  }, 1000);
};

Foo.prototype.bar = promisify(Foo.prototype.bar);
Foo.prototype.baz = promisify(Foo.prototype.baz);

bob.bar(1)
   .then(myCallback)
   .then(bob.baz)
   .then(myCallback)