4

So here's a weird KnockoutJS problem I've never actually come across before.

I'm working on an application that uses Knockout Components very heavily.

In one part of the app, I have an editor page that's built dynamically from a JSON driven backend, and which populates a front end page with a number of widgets, depending on what it's told from the back end data.

Example The back end might send

[{"widget": "textBox"},{"widget": "textBox"},{"widget": "comboBox"},{"widget": "checkBox"}]

Which would cause the front end to build up a page containing

<html>
  ....
  <textbox></textbox>
  <textbox></textbox>
  <combobox></combobox>
  <checkbox></checkbox>
  ....
</html>

Each of the custom tags is an individual KnockoutJS component, compiled as an AMD module and loaded using RequireJS, each component is based on the same boiler plate:

/// <amd-dependency path="text!application/components/pagecontrols/template.html" />
define(["require", "exports", "knockout", 'knockout.postbox', "text!application/components/pagecontrols/template.html"], function (require, exports, ko, postbox) {
    var Template = require("text!application/components/pagecontrols/template.html");
    var ViewModel = (function () {
        function ViewModel(params) {
            var _this = this;
            this.someDataBoundVar = ko.observable("");
        }
        ViewModel.prototype.somePublicFunction = function () {
            postbox.publish("SomeMessage", { data: "some data" });
        };
        return ViewModel;
    })();
    return { viewModel: ViewModel, template: Template };
});

The components communicate with each other and with the page using "Knockout Postbox" in a pub sub fashion.

And when I put them into the page I do so in the following manor:

<div data-bind="foreach: pageComponentsToDisplay">
    <!-- ko if: widget == "textBox" -->
    <textBox params="details: $data"></textBox>
    <!-- /ko -->

    <!-- ko if: widget == "comboBox" -->
    <comboBox params="details: $data"></comboBox>
    <!-- /ko -->

    <!-- ko if: widget == "checkBox" -->
    <checkBox params="details: $data"></checkBox>
    <!-- /ko -->
</div>

and where pageComponentsToDisplay is a simple knockout observable array that I just push the objects received from the backend onto:

pageComponentsToDisplay = ko.observableArray([]);
pageComponentsToDisplay(data);

Where 'data' is as shown in JSON above

Now all of this works great, but here-in now lies the ODD part.

If I have to do a "reload" of the page, I simply

pageComponentsToDisplay = ko.observableArray([]);

to clear the array, and consequently, all my components also disappear from the page, as expected, however when I load the new data in, again using:

pageComponentsToDisplay(data);

I get my new components on screen as expected, BUT the old ones appear to be still present and active in memory, even though there not visible.

The reason I know the controls are still there, because when I issue one of my PubSub messages to ask the controls for some state info, ALL of them reply.

It seems to me that when I clear the array, and when KO clears the view model, it actually does not seem to be destroying the old copies.

Further more, if I refresh again, I then get 3 sets of components responding, refresh again and it's 4, and this keeps increasing as expected.

This is the first time I've encountered this behaviour with knockout, and I've used this kind of pattern for years without an issue.

If you want a good overview of how the entire project is set up, I have a sample skeleton layout on my github page:

https://github.com/shawty/dotnetnotts15

If anyone has any ideas on what might be happening here I'd love to hear them.

As a final note, I'm actually developing all this using Typescript, but since this is a runtime problem, I'm documenting it from a JS point of view.

Regards Shawty

Update 1

So after digging further (and with a little 'new thinking' thanks to cl3m's answer) I'm a little bit further forward.

In my initial post, I did mention that I was using Ryan Niemeyer's excelent PubSub extension for Knockout 'ko postbox'.

It turn's out, that my 'Components' are being disposed of and torn down BUT the subscription handlers that are being created to respond to postbox are not.

The result is, that the VM (or more specifically the values that the subscription uses in the VM) are being kept in memory, along with the postbox subscription handler.

This means when my master broadcasts a message asking for component values, the held reference responds, followed by the visibly active component.

What I need to now do is figure out a way to dispose these subscriptions, which because I'm using postbox directly, and NOT assigning them to an observable in my model, means I don't actually have a var or object reference to target them with.

The quest continues.

Update 2

See my self answer to the question below.

shawty
  • 5,729
  • 2
  • 37
  • 71
  • Are you using the `ko.utils.domNodeDisposal.addDisposeCallback()` method to dispose of your knockout components once their corresponding elements are removed from the `DOM`? – cl3m Mar 21 '16 at 10:03
  • Not that I'm aware of, I just clear the array and I assumed that tore down the instances of my components automatically. I have to confess however when I was researching this on Friday I came across the docs that mentioned it, but have not yet looked at using it. In short, unless KO is doing it automatically for me, then NO I'm not. – shawty Mar 21 '16 at 10:07
  • I'm using this method for custom bindings, I dit not try it on components however... – cl3m Mar 21 '16 at 10:14
  • Your using the same method as me, or your using the addDisposeCallBack? If it's the later would you mind posting a sample? – shawty Mar 21 '16 at 10:18
  • I'm using the `addDisposeCallBack`. See sample in my answer. I'm not sure it will help much though :S – cl3m Mar 21 '16 at 10:34
  • As soon as you mentioned postbox I figured it'd be the culprit (not that it's bad, but it's not a weak subscription so you need to manage it). Glad to see you got it sorted!! :) – Ian Yates Mar 22 '16 at 20:56
  • Try using pageComponentsToDisplay.removeAll() to clear the array and then load it with pageComponentsToDisplay(newData). – Dandy Mar 22 '16 at 22:47
  • The last update to this question is a solution. Shawty, would you move that to an answer, so it can be accepted? Thanks. – halfer Apr 05 '17 at 21:00

2 Answers2

1

I'm not sure this will help but, as per my comment, here is how I use the ko.utils.domNodeDisposal.addDisposeCallback() in my custom bindings. Perhaps there is a way to use it in knockout components:

ko.bindingHandlers.tooltip = {
    init: function(element, valueAccessor) {
      $(element).tooltip(options);
      ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
        $(element).tooltip('destroy');
      });
   }
}

More reading on Ryan Niemeyer's website

cl3m
  • 2,791
  • 19
  • 21
  • Thanks will test and follow that up now, and let you know. – shawty Mar 21 '16 at 10:36
  • Just had a crack with this, not much help, but has given me some ideas. I'm assuming your using this to kill off Bootstrap components? – shawty Mar 21 '16 at 11:04
  • Yep, among others libraries I apply to my elements. Googling for your problem, I've just stumble upon this doc: http://knockoutjs.com/documentation/component-binding.html#disposal-and-memory-management Maybe the `dispose ()` method can help – cl3m Mar 21 '16 at 11:51
  • yea, I'm playing with that possability right now... I can get the Dispose to fire when I need it, but I still don't know how to "Tear Down the Component" and destroy it. (If that makes sense) – shawty Mar 21 '16 at 11:54
  • You may also want to take a look at the unobstructive event handling: http://knockoutjs.com/documentation/unobtrusive-event-handling.html – cl3m Mar 21 '16 at 11:58
  • See my update :-) still not solved yet... but closer now – shawty Mar 21 '16 at 12:37
  • Nice! I didn't posted it, but I was wondering whether or not you were removing your `Postbox`' subscriptions. Seems like you are close to a solution! – cl3m Mar 21 '16 at 12:44
1

The problem it seems was due to Knockout hanging onto subscriptions set up by postbox when the actual components where active.

In my case, I use postbox purely as a messaging platform, so all i'm doing is

ko.postbox.subscribe("foo", function(payload) { ... });

all the time, since I was only ever using single shot subscriptions in this fashion, I was never paying ANY Attention to the values returned by the postbox subscription call.

I did things this way, simply because in many of the components I create there is a common API that they all use, but to which they all respond in different ways, so all I ever needed was a simple this is what to do when your called handler that was component specific, but not application specific.

It turns out however that when you use postbox in this manner, there is no observable for you to target, and as such there is nothing to dispose. (Your not saving the return, so you have nothing to work with)

What the Knockout and Postbox documentation does not mention, is that the return value from postbox.subscribe is a general Knockout subscription function, and by assigning the return from it to a property within your model, you then have a means to call the functionality available on it, one of those functions provides the ability to "dispose" the instance, which NOT ONLY removes the physical manifestation of the component from it's collection, BUT ALSO ensures that any subscriptions or event handlers connected to it are also correctly torn down.

Couple with that, the fact that you can pass a dispose handler to your VM when you register it, the final solution is to make sure you do the following

/// <amd-dependency path="text!application/components/pagecontrols/template.html" />
define(["require", "exports", "knockout", 'knockout.postbox', "text!application/components/pagecontrols/template.html"], function (require, exports, ko, postbox) {
    var Template = require("text!application/components/pagecontrols/template.html");
    var ViewModel = (function () {
        function ViewModel(params) {
            var _this = this;
            this.someDataBoundVar = ko.observable("");
            this.mySubscriptionHandler = ko.postbox.subscribe("foo", function(){
              // do something here to handle subscription message
            });
        }
        ViewModel.prototype.somePublicFunction = function () {
            postbox.publish("SomeMessage", { data: "some data" });
        };
        return ViewModel;
        ViewModel.prototype.dispose = function () {
          this.mySubscriptionHandler.dispose();
        };
        return ViewModel;
    })();
    return { viewModel: ViewModel, template: Template, dispose: this.dispose };
});

You'll notice that the resulting class has a "dispose" function too, this is something that KnockoutJS provides on component classes, and if your class is managed as a component by the main KO library, KO will look for and execute if found, that function when your component class goes out of scope.

As you can see in my example, Iv'e saved the return from the subscription handler as previously mentioned, then in this hook point that we know will get called, used that to ensure that I also call dispose on each subscription.

Of course this ONLY shows one subscription, if you have multiple subscriptions, then you need multiple saves, and multiple calls at the end. An easy way of achieving this, especially if your using Typescript as I am, is to use Typescripts generics functionality and save all your subscriptions into a typed array, meaning at the end all you need to do is loop over that array and call dispose on every entry in it.

shawty
  • 5,729
  • 2
  • 37
  • 71
  • You've probably saved me a very frustrating couple of days trying to work out the reason for this! Thanks so much for this answer! – Jamie Mclaughlan Jul 19 '17 at 15:55
  • Your more than welcome Jamie. This thing had me going around in circles for at least a week. As an added bonus, if your using Aurelia, the process is exactly the same using the eventing library in that. – shawty Jul 19 '17 at 23:06
  • Fantastic. I'm not using Aurelia but may use it for future projects, so thanks for the tip. All the best! Jamie – Jamie Mclaughlan Jul 20 '17 at 08:48
  • If you want a ready to rock and roll app skeleton using Knockout, and the app that this problem was solved for, you'll find it on my github page @ github/shawty – shawty Jul 20 '17 at 10:19
  • PS: Iv'e moved entirely to Aurelia now, it rocks, and it's just awesome, I should probably write some blogs about it... – shawty Jul 20 '17 at 10:20
  • That's great. If you manage to write some Aurelia blog posts in the near future, please let me know. I'll be sure to give them a good read :). Thanks again – Jamie Mclaughlan Jul 20 '17 at 10:38
  • will do, but probs just best to subscribe to "shawtyds.wordpress.com" then that way you'll get notifications :-) I usually tweet them on twitter/@shawty_ds too – shawty Jul 20 '17 at 10:41