5
\$\begingroup\$

I've been researching about JavaScript and jQuery function chaining and different callback techniques. I am trying to create a very generic code that allows chaining, and in a way that I may create several parallel chains without them interfering in one another.

Expected Usage

Setting up

/*---Usage---*/

var chain = new chainFramework();

/*************************************************
 * Tough I am aware async code called inside these functions will continue its way independently,
 * I'd like to be able to make a call and wait for a response here before reporting
 * completeness. Thus, I can't use a return statement. 
 *************************************************/

chain.addIdleFunction( function( optionalParameter ) {
  /***********************************************
   * This code will be executed whenever the chain becomes idle.
   * If a function ends too quick, before another one can be added,
   * the chain will be considered idle. To avoid this, add an array of functions.
   ***********************************************/
});

Array Usage

/*---Add several functions at once to be chained---*/
var sampleFunctionArray = [];

sampleFunctionArray.push( function( optionalParameter ){
  // Synchronous and ansynchronous operations go here.
  chain.functionComplete();
});

sampleFunctionArray.push( function( optionalParameter ){
  // Synchronous and ansynchronous operations go here.
  givenAjaxCall(function(){
    chain.functionComplete();
  });
});

sampleFunctionArray.push( function( optionalParameter ){
  // Synchronous and ansynchronous operations go here.
  $(givenElement).doStuff();
  $(givenElement).on("event", function(){
    chain.functionComplete();
  });
});

sampleFunctionArray.push( function( optionalParameter ){
  //Synchronous and ansynchronous operations go here.
  givenFunctionWithCallback(chain.functionComplete);
});

chain.addFunctionArray(sampleFunctionArray);

Single Function Usage

/*---Add individual functions. This may provide chain behavior, but not necessarily---*/
// Useful for adding functions to the queue after the chain is running.

chain.addFunction( function( optionalParameter ) {
  // Synchronous and ansynchronous operations go here.
  chain.functionComplete();
});

chain.addFunction( function( optionalParameter ) {
  // Synchronous and ansynchronous operations go here.
  givenAjaxCall(function(){
    chain.functionComplete();
  });
});

chain.addFunction( function( optionalParameter ) {
  // Synchronous and ansynchronous operations go here.
  $(givenElement).doStuff();
  $(givenElement).on("event", function(){
    chain.functionComplete();
  });
});

chain.addFunction( function( optionalParameter ) {
  // Synchronous and ansynchronous operations go here.
  givenFunctionWithCallback(chain.functionComplete);
});

Current Status

Here's what I've got so far. I haven't tested it yet, but I think my implementation is solid. I'm a little worried about overhead, but this question is really about approach.

/*---Class-like function, meant to be in a separate file---*/

function chainFramework() {
    var chainObject = {
        idleFunction: null,
        hasInitialized: false,
        currentFunctionQueueIndex: 0,
        functionsToChain: [],

        addFunctionArray: function( functionArray ){
            var isAlreadyFiring = chainObject.hasInitialized;
            for(var i = 0; i < functionArray.length; i++){
                chainObject.functionsToChain.push(functionArray[i]);
            }
            chainObject.checkFiringStatus(isAlreadyFiring);
        },

        addFunction: function( functionCode ){
            var isAlreadyFiring = chainObject.hasInitialized;
            chainObject.functionsToChain.push(functionCode);
            chainObject.checkFiringStatus(isAlreadyFiring);
        },

        addIdleFunction: function( idleFunction ){
            chainObject.idleFunction = idleFunction;
        },

        checkFiringStatus: function(isAlreadyFiring){
            if(!isAlreadyFiring){
                chainObject.fireNextFunction(chainObject.currentFunctionQueueIndex);
                chainObject.hasInitialized = true;
            }
        },

        fireNextFunction: function(functionIndex){
            chainObject.functionsToChain[functionIndex]();
        },

        functionComplete: function () {
            chainObject.currentFunctionQueueIndex++;
            if (chainObject.currentFunctionQueueIndex < chainObject.functionsToChain.length) {
                chainObject.fireNextFunction(chainObject.currentFunctionQueueIndex);
            } else {
                chainObject.hasInitialized = false;
                if(chainObject.idleFunction != null){
                    chainObject.idleFunction();
                    chainObject.idleFunction = null;
                }
            }
        }
    };
    return chainObject;
}

The question

This question is really about approach. Is this queue-like behaviour the best one for this? Should I try implementing a Last In First Out design instead?

I've seen literally TONS of approaches to this problem, including on Stack Overflow, but none of those discussions seemed to present a definitive solution. This one seemed pretty straight forward to me.

It has, though, been brought to my attention that I don't handle errors. How should I? I obviously don't want a timeout for my functions and I can't imagine how to keep it generalized, unless I implement it on each function call.

Please note that unless someone wants to point that they're much better than what I'm doing, I'd like to avoid using Promises. This is because their usage gets complicated when trying to make something as generic as mine. I really am not focusing only on AjaxCalls. I have interest in using this for game cutscenes, level loading, app view managing, application flux, etc. I might get to a point where I have dozens of these chains in an array, plus I'm trying to build it from the ground up for the sake of learning.

\$\endgroup\$
  • \$\begingroup\$ Why does this question look familiar? Did you post this on SO yesterday (I think I commented on it) \$\endgroup\$ – Dan Pantry Sep 25 '15 at 11:44
  • \$\begingroup\$ Yes, I was told to post it here xD \$\endgroup\$ – Maurício D'Angelo Fernandes Sep 25 '15 at 12:49
2
\$\begingroup\$

Ok, so let's start with your questions.

This question is really about approach. Is this queue-like behaviour the best one for this? Should I try implementing a Last In First Out design instead?

I'd like to avoid using Promises.

Solutions depend on the use case. You can't just say you want LIFO because you feel like it. Or I don't like Promises because I don't feel like it. It's like saying "I'd like to play Windows games, but I must use a Snow Leopard on a VM." Use the right approach for the use case. Though I'd say Promises are indeed not suited for this, since once a promise is resolved, it stays resolved.

I've seen literally TONS of approaches to this problem, including on StackOverflow, but none of those discussions seemed to present a definitive solution.

That's because you didn't really define a use case. You aimed generically. We all have our approaches, each with it's own trade-offs and edge cases. If this is your solution, then we agree whatever your reason may be.

It has, tough, been brought to my attention that I don't handle errors. How should I, does anyone know?

There's a lot of room for error in JS. But you have to strike a balance between when you should let the engine do it, or you do it. For instance, addFunctionArray. Your function assumes functionArray is actually an array. JS will throw up if you pass something other than an array, and get a generic message. But it's also meaningful if you did throw it manually, with a meaningful message and point to a helpful link. Again, it's about balance.

I really am not focusing only on AjaxCalls

Who ever said Promises were for AJAX only? Promises are like the if-else of async programming (aside from generators and yields).

I'm trying to build it from the ground up for the sake of learning.

This... is what I like to hear :D

function chainFramework() {
  var chainObject = {
    idleFunction: null,
    hasInitialized: false,
    currentFunctionQueueIndex: 0,
    functionsToChain: [],

Your function looks like a factory, and acts like a factory. Don't use new and don't treat it like a constructor. You could also move the state out of the object and just be local variables inside the function. The closure will take care of hiding that state. You don't want the outside world to be messing with your hasInitialized and throw off your logic.

var isAlreadyFiring = chainObject.hasInitialized;
  for(var i = 0; i < functionArray.length; i++){
    chainObject.functionsToChain.push(functionArray[i]);
  }
chainObject.checkFiringStatus(isAlreadyFiring);

There are several ways to append to an array without calling an operation repeatedly. concat is one way, although it creates a new array containing combined elements. You could also use apply with push like Array.prototype.push.apply(oldArray, newArray).

    checkFiringStatus: function(isAlreadyFiring){
        if(!isAlreadyFiring){
            chainObject.fireNextFunction(chainObject.currentFunctionQueueIndex);
            chainObject.hasInitialized = true;
        }
    },

I'm not sure I'm with this. Your function is called checkFiringStatus but runs the next operation. That sounds counter-intuitive. I just want to check, not run. Why is it running? It doesn't even return a boolean. I'm so confused. :P

chainObject.hasInitialized = false;

Not sure if the wording is right. "Initialized" for me means it already started, and you can't "uninitialize". Maybe isIdle would be a better word.

So far, your code looks pretty good. I'll take a shot at it though:

function chain() {
  // The state will remain in the closure. Your interface can still access them
  // but the outside world can't mutate it directly.
  var chainedFunctions = [];
  var idleFunction = [];
  var isIdle = false;
  var noop = function(){};

  // Factory: Call function, return object.
  return {
    addFunction: function(callback) {
      // This might be a good place to check if `callback` is really a
      // function, and throw an error manually if it's not.
      chainedFunctions.push(callback);
    },
    addFunctionArray: function(callbackArray) {
      // This might be a good place to put validation and check if it's
      // really an array of objects. Throw an error if something wrong
      // happens.
      Array.prototype.push.apply(chainedFunctions, callbackArray);
    },
    addIdleFunction: function(callback) {
      // Now we use an array and just assign to 0. That way, we can just
      // shift it off the array when we need it, and don't have to deal
      // with nulling it.
      idleFunctions[0] = callback;
    },
    checkFiringStatus: function() {
      // Instead of "initialized", I named it "isIdle" because:
      // 1. "initialized" sounds like a one-time thing. "Uninitialized"
      //    sounds like it has never initialized.
      // 2. In programming, prefixing `is` usually indicates a boolean,
      //    which it is.
      return isIdle;
    },
    // You have a next and complete, where complete just fires next.
    // why not merge them?
    fire: function() {
      isIdle = false;

      // Note that we just chain together commands. If we find something
      // queued up, use it. If none, then we turn to the idle function.
      // If no idle function, we execute a noop. A noop is handy so that
      // `next` will always be a function, and we avoid a lot of `if-else`
      var next = chainedFunctions.shift() || idleFunctions.shift() || noop;

      // Note that I've been calling `call` and `apply` with null. That's
      // because determining what `this` is has always been trouble.
      // Let's just agree not to give meaning to `this`.
      next.call(null);

      // Only then do we say we're idle when we're out of functions.
      isIdle = !chainedFunctions.length && !idleFunctions.length;
    }
  };
}
\$\endgroup\$
  • \$\begingroup\$ Wow, that's awesome! Thanks, I'll study this trough and maybe post a much better version later, thank you! \$\endgroup\$ – Maurício D'Angelo Fernandes Sep 30 '15 at 16:21
  • \$\begingroup\$ About my use case. I'm currently using this for implementing a view manager inside web apps. So, this is gonna be used to wait for resource preloading, markdown interpretation, and code assembly. Then I'm gonna use some of these chains to control animation flow, to enter the app and show a splash screen and whatever. My goal with this is to build a full client side view manager from the ground up with no server dependencies which I can later encapsulate into an app. It has its own little markdown language, but I was struggling with flux control, so I built this. \$\endgroup\$ – Maurício D'Angelo Fernandes Sep 30 '15 at 16:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.