Knockout.js's cleanNode does not unbind events

2019-03-04 17:22发布

问题:

I have a modal popup with similar data and need the bindings cleared. The answer for the highest rating says to use knockout's clean node method

Here are (handwritten) code snippets:

var ViewModel = function(v) {
   var self = this;
   self.Foo = ko.observable(v.Foo);
   self.Bar = ko.observable(v.Bar);
   self.Stuffs = ko.observableArray([]);
   self.AddStuffs = function() { ... }
}

var myViewModel = new ViewModel({Foo : "", Bar: "" });
var myModal= document.getElementById("myModal");
ko.cleanNode(myModal);
ko.applyBindings(myViewModel, myModal);

html:

<div id="myModal">
<a href="#" data-bind="click:$root.AddStuff">my link</a>
<table>
    <tbody data-bind="foreach:Stuffs ">
        <tr>
            <td><span data-bind="text:Interval"></span></td>
        </tr>
    </tbody>
</table>
</div>

When I first open the modal, everything seems to work okay. But this answer says cleanNode is the incorrect solution due to cleanNode being knockout's internal cleanup. It doesn't clean up event handlers, so when my modal is closed and opened again and I click the link for AddStuff, the event gets called n times (n = how many times I opened the popup). The proposed solution mentioned was "A better pattern is to use with or the template binding around a section and allow it to be re-rendered with the new bindings." but there was no followup on how to do either.

I'm not sure what he means by "template" but I tried adding "with" to bind my div that I use for the modal and events are still being called multiple times on one link click. Can someone help me find a way to get this working right?

回答1:

I don't think cleanNode is what you want. Generally speaking, you shouldn't need to be cleaning nodes and reapplying bindings unless you're doing something very out of the ordinary, which I don't think you are. You simply want to change the ViewModel that that particular section of the DOM is bound to, correct?

I'd simply make an observable that holds the ViewModel that the modal should be bound to, and then write your template based on that.

var ModalViewModel = function(v) {
   var self = this;
   self.Foo = ko.observable(v.Foo);
   self.Bar = ko.observable(v.Bar);
   self.Stuffs = ko.observableArray([]);
   self.AddStuffs = function() { ... }
}

var modalViewModel = new ModalViewModel({Foo : "Foo", Bar: "Bar" });

var viewModel = {
    modal: ko.observable(modalViewModel)
};

var myModal= document.getElementById("myModal");

ko.applyBindings(myViewModel, myModal);

Note that the viewModel that is actually bound to the modal never changes, but contains a sub-viewModel that does. The above logic should be executed exactly once, then to change or remove the modal, the viewModel.modal observable can be modified.

Then your template would look like

<div id="myModal" data-bind="with: modal">
<a href="#" data-bind="click:AddStuff">my link</a>
<table>
    <tbody data-bind="foreach:Stuffs ">
        <tr>
            <td><span data-bind="text:Interval"></span></td>
        </tr>
    </tbody>
</table>
</div>

(With the only change being the with: modal part, and the removal of an unnecessary $root)

Here's a minimalistic version that demonstrates this working: http://jsfiddle.net/260wypyk/