How to implement reusable callback functions
Asked Answered
L

2

9

I am fairly new to JavaScript and I am working in node which requires a good understanding of async programming and callback design. I have found that using embedded functions is very easy to do even when your callbacks are multiple levels deep. Your embedded callbacks just end up being closures.

However, when you have several layers of callbacks where many of the callbacks are similar across execute routes, you end up rewriting a lot of callback code over and over in separate callback chains. For example, if mycb1 and mycb2 definitions below are moved outside of A, they no longer have implicit access to A's variables and thus, no longer function as closures.

Example with embedded definitions where they function as closures.

mod.A=function(){
  var mycb1=function(err){
    if (!err){
      var mycb2=function(err){
        cb(err);
      };
      mod.foo2(args,mycb2);
    }
    else{cb(err);}
  };
  mod.foo1(args,mycb1);
}

mod.foo1 = function(args,cb){
  //do some work
  cb(err);
};

mod.foo2 = function(args,cb){
  //do some work
  cb(err);
} 
//execute
mod.A();

I want to do the following but be able to change the variable scope for mycb1 and mycb2 functions so they can be used as closures from where ever they are called. For example:

var mycb2=function(err){
  ...
  cb(err);
};

var mycb1=function(err){
  if (!err){        
    mod.foo2(args,mycb2);  //but have it act like a closure to mycb1
  }
  else{cb(err);}
};

mod.A=function(){
  ...
  mod.foo1(args,mycb1);  //but have it act like a closure to A
} 

mod.foo1 = function(args,cb){
  //do some work
  cb(err);
}

mod.foo2 = function(args,cb){
  //do some work
  cb(err);
}

I know that I can implement a design that either sets variable at the mod level so they are accessible to mod level functions. However, this seems to somewhat pollute the mod scope with variable that may only be used by a few of its' methods. I also know that I could pass in variables to make them accessible to the callbacks when they are executed. However, if I understand how JS and callbacks work, I would have to pass those to fooX and then have foo pass them to the callbacks. That doesn't seem like a good plan either. Can the variable scope of a function be changed so it acts like a closure from its point of execution rather than its point of definition? If not, what is the best way to modularize your callback code so it can be reused?

Leshalesher answered 16/1, 2015 at 20:36 Comment(2)
you can use Function.bind() to cement known values into named callbacks at run-time. that's about as close to "closure from its point of execution" as it gets, if i understand you correctly.Michaelamichaele
you can also use Promises to persist state without over-crowding module closuresMichaelamichaele
P
10

In general, there is no getting around having to create another function in-line that has access to the closures. You can create the function in-line by having a simple anonymous function that passes some arguments to the parent callback as parameters while accepting the rest (i.e. a partial function), or by using Function.bind() to create the partial function itself.

For example, if you initially had:

function(...) {
    // closure vars x1, y1
    foo.bar( function(result) {
        // do something with x1 and y1
    });
}

You could extract that to:

var callback = function(x1, y1, result) {
    // do something with x1 and y1
};

function(...) {
    // closure vars x1, y1

    // option 1: make your own partial function
    foo.bar( function(result) { return callback(x1, y1, result); });
    // with ES6: foo.bar( (result) => callback(x1, y1, result); });

    // option 2: use Function.bind
    foo.bar( callback.bind(this, x1, y1); );
}
Photodynamics answered 16/1, 2015 at 20:42 Comment(1)
This is what I needed. Thanks for elaborating the answer. It really helps a newbie :)Leshalesher
M
1

here is a nested callback example with no closure and no nesting. it grabs the page specified, finds the third link, then shows the source of that link to the user.

in this case, when run from here in the console and fed the home page, the logout page is the third link, and it's contents are alerted.

all the callbacks are siblings, and there are no outside state variables, just pure functional async:

// look ma, no nesting!
function ajax(a, c) {
    var e = new XMLHttpRequest;
    e.onload= ajaxOnload.bind(this, c, e);
    e.open( "GET", a, !0);
    e.send();
    return e
}

function ajaxOnload(c, e) {
    c(e.responseText, e);
}

function cbFind3(cb, s){
    var t=document.createElement("body");
    t.innerHTML=s;
    var lnk= t.getElementsByTagName("a")[3].href;
    ajax(lnk, cb);    
}

function grabThirdLinkSource(url, cb){
  ajax(url, cbFind3.bind(this, cb));
}

grabThirdLinkSource("/", alert);

it's not the most useful example, but it does show how to chain functions across invocations with bind(). i used vanilla ajax and avoided promises simply to show how this style of interaction performs without any complications. even the ajax helper uses a non-nested function to feed responseText to the "core" callbacks instead of the event or whole xhr object.

Michaelamichaele answered 16/1, 2015 at 21:7 Comment(2)
Thanks. Your example helps confirm what the 2 previous users suggested. The truth is I wanted to force callbacks to execute with the same variable scope that the calling code was executing in and you can't do that in javascript. You can use partial functions or binding to force object scope but variable access can only be achieved through passing arguments to function parameters.Leshalesher
you can mess with lexical scope to called functions via eval or with, but the functional approach using argument passing typically works better in the long run and for performance by enforcing at least partial encapsulation, re-applicability, and built-in "memory management". it seems a little different at first, and it is, but this continuation style works really well for dependent/related async processes.Michaelamichaele

© 2022 - 2024 — McMap. All rights reserved.