jsPerf.app is an online JavaScript performance benchmark test runner & jsperf.com mirror. It is a complete rewrite in homage to the once excellent jsperf.com now with hopefully a more modern & maintainable codebase.
jsperf.com URLs are mirrored at the same path, e.g:
https://jsperf.com/negative-modulo/2
Can be accessed at:
https://jsperf.app/negative-modulo/2
This test is to check on the performance changes with the nested foreach with the repeat binding
<script src="http://knockoutjs.com/downloads/knockout-3.1.0.js" type="text/javascript"></script>
<script>
// REPEAT binding for Knockout http://knockoutjs.com/
// (c) Michael Best
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
// Version 2.0.0
(function (factory)
{
if (typeof define === 'function' && define.amd)
{
// [1] AMD anonymous module
define(['knockout'], factory);
} else
{
// [2] No module loader (plain <script> tag) - put directly in global namespace
factory(window.ko);
}
})(function (ko)
{
if (!ko.virtualElements)
throw Error('Repeat requires at least Knockout 2.1');
var ko_bindingFlags = ko.bindingFlags || {};
var ko_unwrap = ko.utils.unwrapObservable;
var koProtoName = '__ko_proto__';
if (ko.version >= "3.0.0")
{
// In Knockout 3.0.0, use the node preprocessor to replace a node with a repeat binding with a virtual element
var provider = ko.bindingProvider.instance, previousPreprocessFn = provider.preprocessNode;
provider.preprocessNode = function (node)
{
var newNodes, nodeBinding;
if (!previousPreprocessFn || !(newNodes = previousPreprocessFn.call(this, node)))
{
if (node.nodeType === 1 && (nodeBinding = node.getAttribute('data-bind')))
{
if (/^\s*repeat\s*:/.test(nodeBinding))
{
var leadingComment = document.createComment('ko ' + nodeBinding),
trailingComment = document.createComment('/ko');
node.parentNode.insertBefore(leadingComment, node);
node.parentNode.insertBefore(trailingComment, node.nextSibling);
node.removeAttribute('data-bind');
newNodes = [leadingComment, node, trailingComment];
}
}
}
return newNodes;
};
}
ko.virtualElements.allowedBindings.repeat = true;
ko.bindingHandlers.repeat = {
flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual,
init: function (element, valueAccessor, allBindingsAccessor, xxx, bindingContext)
{
// Read and set fixed options--these options cannot be changed
var repeatParam = ko_unwrap(valueAccessor());
if (repeatParam && typeof repeatParam == 'object' && !('length' in repeatParam))
{
var repeatIndex = repeatParam.index,
repeatData = repeatParam.item,
repeatStep = repeatParam.step,
repeatReversed = repeatParam.reverse,
repeatBind = repeatParam.bind,
repeatInit = repeatParam.init,
repeatUpdate = repeatParam.update;
}
// Set default values for options that need it
repeatIndex = repeatIndex || '$index';
repeatData = repeatData || ko.bindingHandlers.repeat.itemName || '$item';
repeatStep = repeatStep || 1;
repeatReversed = repeatReversed || false;
var parent = element.parentNode, placeholder;
if (element.nodeType == 8)
{ // virtual element
// Extract the "children" and find the single element node
var childNodes = ko.utils.arrayFilter(ko.virtualElements.childNodes(element), function (node) { return node.nodeType == 1; });
if (childNodes.length !== 1)
{
throw Error("Repeat binding requires a single element to repeat");
}
ko.virtualElements.emptyNode(element);
// The placeholder is the closing comment normally, or the opening comment if reversed
placeholder = repeatReversed ? element : element.nextSibling;
// The element to repeat is the contained element
element = childNodes[0];
} else
{ // regular element
// First clean the element node and remove node's binding
var origBindString = element.getAttribute('data-bind');
ko.cleanNode(element);
element.removeAttribute('data-bind');
// Original element is no longer needed: delete it and create a placeholder comment
placeholder = document.createComment('ko_repeatplaceholder ' + origBindString);
parent.replaceChild(placeholder, element);
}
// extract and remove a data-repeat-bind attribute, if present
if (!repeatBind)
{
repeatBind = element.getAttribute('data-repeat-bind');
if (repeatBind)
{
element.removeAttribute('data-repeat-bind');
}
}
// Make a copy of the element node to be copied for each repetition
var cleanNode = element.cloneNode(true);
if (typeof repeatBind == "string")
{
cleanNode.setAttribute('data-bind', repeatBind);
repeatBind = null;
}
// Set up persistent data
var lastRepeatCount = 0,
notificationObservable = ko.observable(),
repeatArray, arrayObservable;
if (repeatInit)
{
repeatInit(parent);
}
var subscribable = ko.computed(function ()
{
function makeArrayItemAccessor(index)
{
var f = function (newValue)
{
var item = repeatArray[index];
// Reading the value of the item
if (!arguments.length)
{
notificationObservable(); // for dependency tracking
return ko_unwrap(item);
}
// Writing a value to the item
if (ko.isObservable(item))
{
item(newValue);
} else if (arrayObservable && arrayObservable.splice)
{
arrayObservable.splice(index, 1, newValue);
} else
{
repeatArray[index] = newValue;
}
return this;
};
// Pretend that our accessor function is an observable
f[koProtoName] = ko.observable;
return f;
}
function makeBinding(item, index, context)
{
return repeatArray
? function () { return repeatBind.call(bindingContext.$data, item, index, context); }
: function () { return repeatBind.call(bindingContext.$data, index, context); }
}
// Read and set up variable options--these options can change and will update the binding
var paramObservable = valueAccessor(), repeatParam = ko_unwrap(paramObservable), repeatCount = 0;
if (repeatParam && typeof repeatParam == 'object')
{
if ('length' in repeatParam)
{
repeatArray = repeatParam;
repeatCount = repeatArray.length;
} else
{
if ('foreach' in repeatParam)
{
repeatArray = ko_unwrap(paramObservable = repeatParam.foreach);
if (repeatArray && typeof repeatArray == 'object' && 'length' in repeatArray)
{
repeatCount = repeatArray.length || 0;
} else
{
repeatCount = repeatArray || 0;
repeatArray = null;
}
}
// If a count value is provided (>0), always output that number of items
if ('count' in repeatParam)
repeatCount = ko_unwrap(repeatParam.count) || repeatCount;
// If a limit is provided, don't output more than the limit
if ('limit' in repeatParam)
repeatCount = Math.min(repeatCount, ko_unwrap(repeatParam.limit)) || repeatCount;
}
arrayObservable = repeatArray && ko.isObservable(paramObservable) ? paramObservable : null;
} else
{
repeatCount = repeatParam || 0;
}
// Remove nodes from end if array is shorter
for (; lastRepeatCount > repeatCount; lastRepeatCount -= repeatStep)
{
ko.removeNode(repeatReversed ? placeholder.nextSibling : placeholder.previousSibling);
}
// Notify existing nodes of change
notificationObservable.notifySubscribers();
// Add nodes to end if array is longer (also initially populates nodes)
for (; lastRepeatCount < repeatCount; lastRepeatCount += repeatStep)
{
// Clone node and add to document
var newNode = cleanNode.cloneNode(true);
parent.insertBefore(newNode, repeatReversed ? placeholder.nextSibling : placeholder);
newNode.setAttribute('data-repeat-index', lastRepeatCount);
// Apply bindings to inserted node
if (repeatArray && repeatData == '$data')
{
var newContext = bindingContext.createChildContext(makeArrayItemAccessor(lastRepeatCount));
} else
{
var newContext = bindingContext.extend();
if (repeatArray)
newContext[repeatData] = makeArrayItemAccessor(lastRepeatCount);
}
newContext[repeatIndex] = lastRepeatCount;
if (repeatBind)
{
var result = ko.applyBindingsToNode(newNode, makeBinding(newContext[repeatData], lastRepeatCount, newContext), newContext, true),
shouldBindDescendants = result && result.shouldBindDescendants;
}
if (!repeatBind || (result && shouldBindDescendants !== false))
{
ko.applyBindings(newContext, newNode);
}
}
if (repeatUpdate)
{
repeatUpdate(parent);
}
}, null, { disposeWhenNodeIsRemoved: placeholder });
return { controlsDescendantBindings: true, subscribable: subscribable };
}
};
});
</script>
<table id="testCase1" data-bind="foreach: items1">
<tr data-bind="foreach: columns">
<td data-bind="text: $data" />
</tr>
</table>
<table id="testCase2" data-bind="foreach: items2">
<tr>
<td data-bind="text: columns[0]" />
<td data-bind="text: columns[1]" />
<td data-bind="text: columns[2]" />
<td data-bind="text: columns[3]" />
<td data-bind="text: columns[4]" />
<td data-bind="text: columns[5]" />
<td data-bind="text: columns[6]" />
<td data-bind="text: columns[7]" />
<td data-bind="text: columns[8]" />
<td data-bind="text: columns[9]" />
</tr>
</table>
<table id="testCase3">
<tr data-bind="repeat: { foreach: items3, item: '$row' }">
<td data-bind="repeat: { foreach: $row().columns, item: '$col' }"
data-repeat-bind="text: $col"></td>
</tr>
</table>
<script>
var model = {
items1: ko.observable([]),
items2: ko.observable([]),
items3: ko.observable([])
};
var items = [];
for (var i = 0; i < 200; i++) {
var item = { columns : [] };
for (var j = 0; j < 10; j++) {
item.columns.push(i.toString() + ":" + j.toString());
}
items.push(item);
}
ko.applyBindings(model);
</script>
Ready to run.
Test | Ops/sec | |
---|---|---|
Nested foreach binding |
| ready |
Expanded loop markup |
| ready |
Repeat binding |
| ready |
You can edit these tests or add more tests to this page by appending /edit to the URL.