knockout nested foreach and repeat bindings (v2)

Revision 2 of this benchmark created by Rupesh Kokal on


Description

This test is to check on the performance changes with the nested foreach with the repeat binding

Preparation HTML

<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>

Test runner

Ready to run.

Testing in
TestOps/sec
Nested foreach binding
model.items1(items);
model.items1([]);
ready
Expanded loop markup
model.items2(items);
model.items2([]);
ready
Repeat binding
model.items3(items);
model.items3([]);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 2: published by Rupesh Kokal on