2

I've created a simple observer model in a JavaScript WebApp to handle event-listeners on a more complex JS-Object model (no DOM events). One can register event listener functions that are then stored in an array. By calling a member function out of the wider application of the model the event listeners are executed. So far so good. Here's the implementation that works well:

var ModelObserver = function() {
    this.locationObserverList = [];
}

ModelObserver.prototype.emitEvent = function(eventtype, data) {
    for(var i=0; i < this.locationObserverList.length; i++) {
      var fns = this.locationObserverList[i];
      fns(data);  // function is being called
    }
};

ModelObserver.prototype.registerLocationListener = function( fn) {
    this.locationObserverList.push(fn);
};

If tested it with two listeners in a small sample html site, all good.

Now I want to make the call to the function asynchronously. I tried to change the code of the respective function as follows:

ModelObserver.prototype.emitEvent = function(eventtype, data) {
    for(var i=0; i < this.locationObserverList.length; i++) {
      var fns = this.locationObserverList[i];
      setTimeout(function() {fns(data);}, 0);
    }
};

Unfortunately I have a problem here: only the second listener is being called, but now twice. It seems to be a conflict with the fns variable, so I tried this:

ModelObserver.prototype.emitEvent = function(eventtype, data) {
    var fns = this.locationObserverList;
    for(var i=0; i < this.locationObserverList.length; i++) {
      setTimeout(function() {fns[i](data);}, 0);
    }
};

Now I get an error: "Uncaught TypeError: Property '2' of object [object Array] is not a function".

Does anyone have an idea how to get this working asynchronously?

user_ja
  • 41
  • 1
  • 4

3 Answers3

1

The anonymous function you're giving setTimeout has an enduring reference to the variables it closes over, not a copy of them as of when it was created.

You need to make it close over something else. Usually, you use a function that builds the function for setTimeout and closes over args to the builder:

ModelObserver.prototype.emitEvent = function(eventtype, data) {
    for(var i=0; i < this.locationObserverList.length; i++) {
      var fns = this.locationObserverList[i];
      setTimeout(buildHandler(fns, data), 0);
      // Or combining those two lines:
      //setTimeout(buildHandler(this.locationObserverList[i], data), 0);
    }
};

function buildHandler(func, arg) {
    return function() {
        func(arg);
    };
}

There, we call buildHandler with a reference to the function and the argument we want it to receive, and buildHandler returns a function that, when called, will call that function with that argument. We pass that returned function into setTimeout.

You can also do this with ES5's Function#bind, if you're in an ES5 environment (or include an appropriate shim, as this is shimmable):

ModelObserver.prototype.emitEvent = function(eventtype, data) {
    for(var i=0; i < this.locationObserverList.length; i++) {
      var fns = this.locationObserverList[i];
      setTimeout(fns.bind(undefined, data), 0);
      // Or combining those two lines:
      //setTimeout(this.locationObserverList[i].bind(undefined, data), 0);
    }
};

Skipping some details, that basically does what buildHandler above does.

More on this (on my blog): Closures are not complicated


Side note: By scheduling these functions to be called later via setTimeout, I don't think you can rely on them being called in order. That is, even if you schedule 1, 2, and 3, I don't know that you can rely on them being called that way. The (newish) spec for this refers to a "list" of timers, suggesting order, and so one might be tempted to think that registering timers in a particular order with the same timeout would have them execute in that order. But I don't (skimming) see anything in the spec guaranteeing that, so I wouldn't want to rely on it. A very quick and dirty test suggested the implementations I tried it on did that, but it's not something I'd rely on.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Don't forget that _setTimeout_ also accepts `setTimeout(fn, delay, param1, param2, ...)` in a standards-compliant browser, and it may be worth mentioning that by using _setTimout_ inside the loop you can no-longer guarantee the order of invocation. – Paul S. Apr 09 '14 at 10:07
  • @PaulS.: That was a relatively recent innovation (Mozilla did it a long time ago, others only recently), significant browsers in the wild (such as IE8, which despite today being The Big Day for XP will remain significant for some time to come, having 6-21% global share depending on who you ask). – T.J. Crowder Apr 09 '14 at 10:10
  • @PaulS.: *"...you can no-longer guarantee the order of invocation..."* Good point. Although [the (new) spec](http://www.w3.org/TR/html5/webappapis.html#timers) in this area calls it a "list" of timers, implying order, and so in theory timers registered in a given order with the same timeout value should happen in order, I don't see anything in the spec requiring that. [Seems to be what happens](http://jsbin.com/zavavope/1), but I wouldn't want to rely on it. :-) – T.J. Crowder Apr 09 '14 at 10:18
  • I thought that the list was so you could reference to it with the related _clearTimeout_. Even if that's the case, the function almost certainly won't have finished by the time the second starts if they're both set to the same time. – Paul S. Apr 09 '14 at 10:29
  • 1
    @T.J.Crowder: Thanks you, that's exactly what I need and it works perfectly. As the listeners run independently I do not have any requirements with regards to the execution order. – user_ja Apr 09 '14 at 11:17
0
ModelObserver.prototype.emitEvent = function(eventtype, data) {
     var fns = this.locationObserverList;
     for(var i=0; i < this.locationObserverList.length; i++) {
         (function(j){
             setTimeout(function() {fns[i](data);}, 0);
         }(i));
    }
};

Try this

mulla.azzi
  • 2,676
  • 4
  • 18
  • 25
0

The second try is not going to work. In your first sample try -

setTimeout(function() {this.fns(data);}, 0);