0
votes

In my html view I bind a html table with knockout viewmodel, that contains 3 observableArrays, as following:

function myViewModel() {
    var self = this;
    self.RowIds = ko.observableArray();
    self.ColIds = ko.observableArray();

    self.allCellsList = ko.observableArray();

    self.calculateText = function (r_id, c_id) {
        console.log('calculateText');
        var item = ko.utils.arrayFirst(self.allCellsList(), function (i) {
            return i.rowId() == r_id && i.colId() == c_id;
        });
        return item.text();
    }

    self.loadData = function (rowIds, colIds, allCellsList) {

        //remove old data (not necessary, only for testing , to see how long it takes)
        self.allCellsList.removeAll();

        //populate new data:
        self.RowIds(ko.mapping.fromJS(rowIds));
        self.ColIds(ko.mapping.fromJS(colIds));
        self.allCellsList(ko.mapping.fromJS(allCellsList));
    }

}

RowIds contains list of ids for each row in the table, CoIds contains list of ids for each column in table. allCellsList is the main table that holds list of objects, where each of them contain two identifiers: rowId and colId. The function calculateText is called in each cell in table , and calculates the matching data in allCellsList by these 2 fields. As following:

<table>
    <tbody data-bind="foreach:RowIds">
        <tr data-bind="foreach:$root.ColIds()">
            <td>
                <div data-bind="text:$root.calculateText($data , $parent)">
                </div>
            </td>
        </tr>
    </tbody>
</table>

When I initialize the allCellsList with new data in loadData function, by removing the old data and fetching the new data (or by directly fetching the new data, without using the removeAll function) – the calculateText function is called, For each item in the old list , and for each item in the new list. If the old list contains a huge amount of records – the clear array operation takes too long. (sometimes 3-4 seconds)

My question is how can I populate the allCellsList in that case or in other , so that the reloading operation will be faster? Is there a way to avoid calling the function calculateText , when allCellsList is being cleared?

Thank you.

1
Large tables are, themselves, a performance issue in Knockout. See knockout-table for a better-performing way of rendering tables in Knockout. - Roy J

1 Answers

0
votes

The calculateText function shouldn't be running when calling RowIds.removeAll and ColIds.removeAll. However, it will run when calling self.allCellsList.removeAll, since there's a dependency to this array.

To get only one calculation, do things in this order:

self.loadData = function (rowIds, colIds, allCellsList) {
    self.RowIds.removeAll();
    self.ColIds.removeAll();

    // You don't need this and I'd suggest you removing it
    // self.allCellsList.removeAll();

    // First load the data that `calculateText` depends on
    self.allCellsList(ko.mapping.fromJS(allCellsList));

    // Calculate for these only when added to DOM
    self.ColIds(ko.mapping.fromJS(colIds));
    self.RowIds(ko.mapping.fromJS(rowIds));

}

Edit, to further make my point: Every time you clear vals when there is data in the DOM, you get a recalc for each cell... If you make sure there's no data in the DOM when you set vals, you only get one per cell.

var valSource = { 
  a: { "1": "A1", "2": "A2", "3": "A3" }, 
  b: { "1": "B1", "2": "B2", "3": "B3" }, 
  c: { "1": "C1", "2": "C2", "3": "C3" }
};
var colSource = [{ id: "a" }, { id: "b" }, { id: "c" }];
var rowSource = [{ id: "1"   }, { id: "2" }];

var cols = ko.observableArray([]);
var rows = ko.observableArray([]);
var vals = ko.observable({});
var i = 0;

var getCellValue = function(col, row) {  
  i = i + 1;
  var col = vals()[col.id] || {};
  return col[row.id];
};


var updateSlow = function() {
  // Clear
  vals({}); // Triggers a recalc for every cell
  rows([]); // Remove these freshly recalced cells...
  cols([]); // Does nothing
  
  // Set
  rows(rowSource.slice(0)); // Adds rows
  cols(colSource.slice(0)); // Adds cells and recalcs
  vals(Object.assign({}, valSource)); // Recalcs again
  
  console.log("Updates slow:", i);
  i = 0;
};

var updateFast = function() {
  // Clear
  rows([]);
  cols([]);
  
  // Set
  vals(Object.assign({}, valSource)); // No DOM, so no recalc
  rows(rowSource.slice(0)); // Add rows
  cols(colSource.slice(0)); // Add cells and calculate once

  console.log("Updates fast:", i);
  i = 0;
}

ko.applyBindings({ cols: cols, rows: rows, getCellValue: getCellValue, updateSlow: updateSlow});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<button data-bind="click: updateSlow">update slow</button>
<button data-bind="click: updateFast">update fast</button>

<table data-bind="foreach: rows">
  <tr data-bind="foreach: cols">
    <td data-bind="text: getCellValue($data, $parent)"></td>
  </tr>
</table>