principlejavascriptMinor
Process chain approach
Viewed 0 times
approachprocesschain
Problem
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
Array Usage
Single Function Usage
```
/*---Add individual functions.
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.
Solution
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,
I really am not focusing only on AjaxCalls
Who ever said Promises were for AJAX only? Promises are like the
I'm trying to build it from the ground up for the sake of learning.
This... is what I like to hear :D
Your function looks like a factory, and acts like a factory. Don't use
There are several ways to append to an array without calling an operation repeatedly.
I'm not sure I'm with this. Your function is called
Not sure if the wording is right. "Initialized" for me means it already started, and you can't "uninitialize". Maybe
So far, your code looks pretty good. I'll take a shot at it though:
``
// which it is.
return isIdle;
},
//
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. :PchainObject.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;
},
//
Code Snippets
function chainFramework() {
var chainObject = {
idleFunction: null,
hasInitialized: false,
currentFunctionQueueIndex: 0,
functionsToChain: [],var isAlreadyFiring = chainObject.hasInitialized;
for(var i = 0; i < functionArray.length; i++){
chainObject.functionsToChain.push(functionArray[i]);
}
chainObject.checkFiringStatus(isAlreadyFiring);checkFiringStatus: function(isAlreadyFiring){
if(!isAlreadyFiring){
chainObject.fireNextFunction(chainObject.currentFunctionQueueIndex);
chainObject.hasInitialized = true;
}
},chainObject.hasInitialized = false;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;
}
};
}Context
StackExchange Code Review Q#105679, answer score: 2
Revisions (0)
No revisions yet.