简体   繁体   中英

Multiple Sequential Async JavaScript Functions

Let's say I have a function that looks like this:

var foo = function(callback) {
  var final = {};

  asyncFuncOne(function(x) {
    final.x = x;
  });

  asyncFuncTwo(function(y) {
    final.y = y;
  });

  callback(final);
});

Obviously, this doesn't do what I want it to do (call callback on final when it has both x and y). I have several questions:

  1. Is there a way to do what I want it to do without nesting everything?
  2. Does the current form introduce a race condition? Are both async functions accessing the same final ?

Approach #0. Painful life without promises. Yet life

Actually, your code like cries to be rewritten in promises. Trust me, this refactoring is something you 100% need. But ok, let's try to solve this particular problem without invoking promises at all - just as an exercise. Actually before the promise era the pattern was to introduce a special function that checks whether we can consider that we are done or not.

In your particular case such function is:

function weAreDone() {
   return final.hasOwnPropery('x') && final.hasOwnProperty('y')
}

Then we can introduce asyncFuncDecorator:

function asyncFuncDecorator = function(asyncFunc, asyncFuncHandler) {
   return function(doneFunc, doneHandler) {
       asyncFunc(asyncFuncHandler);
       if (doneFunc()) {
          doneHandler();
       }
   }
}

With this two functions introduced you can write something like:

var foo = function(callback) {
  var final = {};

  //here goes abovementioned declarations
  ... 

  asyncFuncDecorator(asyncFuncOne, function(x) {
    final.x = x;
  })(weAreDone, callback);

  asyncFuncDecorator(asyncFuncTwo, function(y) {
    final.y = y;
  })(weAreDone, callback);

});

You can keep working on making this approach more flexible and universal but, once again, trust me, you'll end up with something very similar to promises, so better promises ;)

Approach #1. Promisifying existing functions

If, for some reason, you are not ready to rewrite all you functions from callback style to promises, you can promisify existing functions by using, once again, a decorator. Here's how it can be done for native Promises, which are present in all modern browsers already (for alternatives, check this question ):

function promisify(asyncCall){
    return new Promise(function(resolve,reject){
         asyncCall(resolve,reject);
    });
}

In that case you can rewrite you code in this fashion:

var foo = function(callback) {

      //here goes abovementioned declarations
      ... 

      Promise.all([promisify(asyncFuncOne), promisify(asyncFuncTwo)]).then(function(data) {
          // by the way, I'd rather not to call any variable "final" ))
          final.x = data[0];
          final.y = data[1];
      }).then(callback);

    });

Not to say that actually foo it's better to be promisified itself ;)

Approach #2. Promises everywhere. From the very beginning

It worth to reiterate this thought - as soon as you need to trigger some function after N other async functions should be completed - promises in 99% cases are unbeatable. It almost always worth trying to rewrite existing code to in promise-based style. Here's how can such code look like

Promise.all([asyncFuncOne(), asyncFuncTwo()]).then(function(data) {

  return Promise.resolve({
    x: data[0],
    y: data[1] 
  })

}).then(callback);

See how much better it become. Also, a common mistake of using promises - is to have a sequential waterfall of thens - retrieving first chunk of data, only after that - the second one, after that - the third one. You actually never should do this unless you are transforming data received in Nth request depending on what you've got in one of your previous requests - instead just use all method.

This is very crucial to understand. This is one of main reasons why promises quite often are misunderstood as something excessively complicated.

Sidenote: as of December'14, native Promises are natively supported by all major modern browsers except IE, and in Node.js has native promise support is a thing since version 0.11.13, so in real-life you still most probably will need to use promise library. There's a lot of Promise spec implementations, you can check this page for the list of standalone promise libraries, it's quite big, the most popular solutiona are, I guess, Q and bluebird.

Approach #3. Generators. Our bright future. Well, may be

This is something worth to mention, generators are de-facto supported in Firefox, Chromium-based browsers and node.js (called with --harmony_generators option). So, de-facto, there are cases when generators can be used, and actually are already used, in production code. It's just that if you are writing a general-purpose web app, you should be aware of this approach but you'll probably won't use it for a while. So, you can use the fact that generators in js allow you to invoke two-way communication through yield/iterator.next(). In that case.

function async(gen) {
    var it = gen();
    var state = it.next();

    var next = function() {
        if (state.done) {
            return state.value;
        };  
        state.value(function(res) {
            state = it.next(res);   
            next();
        }); 
    }   

    next();
}

async(function* () {
    var res = { 
        x: yield asyncFuncOne,
        y: yield asyncFuncTwo
    }   

    callback(res);
});

Actually, there are already dozens of libraries which do this generator wrapping job for you. You can read more about this approach and related libraries here .

final.x and final.y are set on final, but after it's sent to callback so, unless the callback is waiting, x and y are undefined when callback receives them.

You could check to see if one has come back in the response of the others and call out to the callback:

var foo = function(callback) {
  var final = {};

  asyncFuncOne(function(x) {
      final.x = x;
      if (typeof final.y !== 'undefined') {
          callback(final);
      }
  });

  asyncFuncTwo(function(y) {
      final.y = y;
      if (typeof final.x !== 'undefined') {
          callback(final);
      }
  });
});

You could nest your callbacks, though this will cause asyncfuncTwo to not be called until asyncfuncOne has finished):

var foo = function(callback) {
  var final = {};

  asyncFuncOne(function(x) {
      final.x = x;
      asyncFuncTwo(function(y) {
        final.y = y;
        callback(final);
      });
  });
});

Then there are Promises. These are the future of async however they are not fully supported across all browsers (namely, all of IE [11 and below at the this time]). In fact, 40% of all browser users are not using a browser that natively supports Promises . This means you will have to use a polyfill library to give you support adding substantial filesize to your page. For this simple problem and at this given time I wouldn't recommend using Promises for this simple issue. However, you should definitely read up on how they are used.

If you want to see what that could look like, it'd be this:

var asyncFuncOne = function() {
    return new Promise(function(resolve, reject) {
        // A 500 seconds async op and resolve x as 5
        setTimeout(function() { resolve(5); }, 500);
    });
};
var asyncFuncTwo = function() {
    return new Promise(function(resolve, reject) {
        // A 750ms async op and resolve y as 10
        setTimeout(function() { resolve(10); }, 750);
    });
};
var foo = function() {
    var final = {};
    return new Promise(function(resolve, reject) {
        Promise.all([
            asyncFuncOne(),
            asyncFuncTwo()
        ]).then(function(values) {
            final.x = values[0];
            final.y = values[1];
            resolve(final);
        });
    });
};

foo().then(function(final) {
    // After foo()'s Promise has resolved (750ms)
    console.log(final.x + ', ' + final.y); 
});

Note no callbacks, just use of then . In a real scenario you would also use catch and reject . Read more about Promises here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise but, again, I personally don't see a strong need to use them for this single, specific issue (but, to each their own).

Another solution is to create a setter:

var foo = function (callback) {
    var final = {
        setter: function(attr,value){
            this[attr] = value;
            if (this.hasOwnProperty("x") && this.hasOwnProperty("y"))
                callback(this);
        }
    };

    asyncFuncOne(function(x) {
        final.setter("x", x);
    });

    asyncFuncTwo(function(y) {
        final.setter("y", y);
    });
};

一个非常糟糕的主意,但是我以前必须使用它,因为我不会为单个函数导入一个50k的promise库,而是要设置一个循环超时,以检查是否设置了所有必需的变量,然后调用回调。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM