Search code examples
memoryknockout.jsknockout-mapping-plugin

Knockout JS - observing a mutation in any property of an array without ko.mapping


In my application I have to show only the top 5 items of a long list that have the highest rating. I have implemented this as follows:

The long list is an array that I have turned into an observable array with all elements as observables with ko.mapping, and the top 5 items are a computed array that depends on the long list. Whenever anything in the long list changes the computed array resorts the long list and takes the top 5 items.

My problem is that the long array with ko.mapping takes up 60MB of memory, whereas without ko.mapping it only takes 4MB. Is there a way to achieve this effect without ko.mapping the long array?

Here is a fiddle in which I have recreated the scenario with a smaller and simpler long array, but it's just for you to understand what I'm talking about.

This is the long array, for the demo I've made it 12 elements long:

 this.longArray = ko.mapping.fromJS([
    {name:"Annabelle"},
    {name:"Vertie"},
    {name:"Charles"},
    {name:"John"},
    {name:"AB"},
    {name:"AC"},
    {name:"AD"},
    {name:"AE"},
    {name:"AF"},
    {name:"AG"},
    {name:"AH"},
    {name:"AJ"}
]);

And this is the computed array(showing only the top 5):

this.sortedTopItems = ko.computed(function() {
    return self.longArray().sort(function(a, b) { 
        if(a.name() < b.name()) return -1;
            if(a.name() > b.name()) return 1;
            return 0;
    }).slice(0, 5);
}, this);

The change one button is to simulate the long array changing and the reset button is to reset the array to its initial state.


Solution

  • You sure can, but the simplest way would be to filter the data before putting into knockout. If you only ever care about the first 5. Let's assume your long array of items is called data. Note that I'm not able to test this right now, but it should give you a good idea.

    const sortedTopItems = ko.observableArray([]);
    
    // Call with new data
    const update = (data) => {
        data.sort((a,b) => a.name - b.name);
        sortedTopItems(data.slice(0, 5));
    }
    

    This handles the case for simple data where it's not observable. If you want the actual data items (rows) to be observable then I'd do the following:

    const length = 5;
    
    // Create an empty array and initialize as the observable
    const startData = new Array(length).map(a => ({}));
    const sortedTopItems = ko.observableArray(startData);
    
    // Call with new data
    const update = (data) => {
        data.sort((a,b) => a.name - b.name);
    
        for(let i = 0; i < length; i++) {
            const item = sortedTopItems()[i];
            ko.mapping.fromJS(data[i], item); // Updates the viewModel from data
        }
    }