ko.viewmodel vs ko.mapping vs knockout.wrap

JavaScript performance comparison

Revision 14 of this test case created

Info

Performance comparison between the knockout mapping plugins ko.viewmodel and ko.mapping and knockout.wrap

Preparation code

<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.3.5/knockout.mapping.js"></script>
<script src="http://coderenaissance.github.com/knockout.viewmodel/knockout.viewmodel.min.js"></script>
<script>
// Knockout Fast Mapping v0.1
// License: MIT (http://www.opensource.org/licenses/mit-license.php)

(function (factory) {
        // Module systems magic dance.

        if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
                // CommonJS or Node: hard-coded dependency on "knockout"
                factory(require("knockout"), exports);
        } else if (typeof define === "function" && define["amd"]) {
                // AMD anonymous module with hard-coded dependency on "knockout"
                define(["knockout", "exports"], factory);
        } else {
                // <script> tag: use the global `ko` object, attaching a `wrap` property
                factory(ko, ko.wrap = {});
        }
}(function (ko, exports) {
   
    // this function mimics ko.mapping
    exports.fromJS = function(jsObject, computedFunctions)
    {
        reset();
        return wrap(jsObject, computedFunctions);
    }

    // this function unwraps the outer for assigning the result to an observable
    // see https://github.com/SteveSanderson/knockout/issues/517
    exports.updateFromJS = function(observable, jsObject, computedFunctions)
    {
        reset();
        return observable(ko.utils.unwrapObservable(wrap(jsObject, computedFunctions)));
    }

    exports.fromJSON = function (jsonString, computedFunctions) {
        var parsed = ko.utils.parseJson(jsonString);
        arguments[0] = parsed;
        return exports.fromJS.apply(this, computedFunctions);
    };
   
    exports.toJS = function (observable) {
        return unwrap(observable);
    }

    exports.toJSON = function (observable) {
        var plainJavaScriptObject = exports.toJS(observable);
        return ko.utils.stringifyJson(plainJavaScriptObject);
    };

    function typeOf(value) {
        var s = typeof value;
        if (s === 'object') {
            if (value) {
                if (value.constructor == Date)
                    s = 'date';
                else if (Object.prototype.toString.call(value) == '[object Array]')
                    s = 'array';
            } else {
                s = 'null';
            }
        }
        return s;
    }

    // unwrapping
    function unwrapObject(o)
    {
        var t = {};

        for (var k in o)
        {
            var v = o[k];

            if (ko.isComputed(v))
                continue;

            t[k] = unwrap(v);
        }

        return t;
    }

    function unwrapArray(a)
    {
        var r = [];

        if (!a || a.length == 0)
            return r;
       
        for (var i = 0, l = a.length; i < l; ++i)
            r.push(unwrap(a[i]));

        return r;
    }

    function unwrap(v)
    {
        var isObservable = ko.isObservable(v);

        if (isObservable)
        {
            var val = v();

            if (typeOf(val) == "array")
            {
                return unwrapArray(val);
            }
            else
            {
                return val;
            }
        }
        else
        {
            if (typeOf(v) == "array")
            {
                return unwrapArray(v);
            }
            else if (typeOf(v) == "object")
            {
                return unwrapObject(v);
            }
            else
            {
                return v;
            }
        }
    }

    function reset()
    {
        parents = [{obj: null, wrapped: null, lvl: ""}];
    }    
   
    // wrapping

    function wrapObject(o, computedFunctions)
    {
        // check for infinite recursion
        for (var i = 0; i < parents.length; ++i) {
            if (parents[i].obj === o) {
                return parents[i].wrapped;
            }
        }

        var t = {};

        for (var k in o)
        {
            var v = o[k];

            parents.push({obj: o, wrapped: t, lvl: currentLvl() + "/" + k});

            t[k] = wrap(v, computedFunctions);

            parents.pop();
        }

        if (computedFunctions && computedFunctions[currentLvl()])
            t = computedFunctions[currentLvl()](t);

        if (hasES5Plugin())
            ko.track(t);

        return t;
    }

    function wrapArray(a, computedFunctions)
    {
        var r = ko.observableArray();

        if (!a || a.length == 0)
            return r;

        for (var i = 0, l = a.length; i < l; ++i)
            r.push(wrap(a[i], computedFunctions));

        return r;
    }

    // a stack, used for two purposes:
    //  - circular reference checking
    //  - computed functions
    var parents;

    function currentLvl()
    {
        return parents[parents.length-1].lvl;
    }

    function wrap(v, computedFunctions)
    {
        if (typeOf(v) == "array")
        {
            return wrapArray(v, computedFunctions);
        }
        else if (typeOf(v) == "object")
        {
            return wrapObject(v, computedFunctions);
        }
        else
        {
            if (!hasES5Plugin())
            {
                var t = ko.observable();
                t(v);
                return t;
            } else
                return v;
        }
    }

    function hasES5Plugin()
    {
        return ko.track != null;
    }
}));
</script>
<script>
Benchmark.prototype.setup = function() {
    var numberOfArrayRecords = 100,
      viewmodel = null,
      model = {
        items: []
      };
   
    for (var x = 0; x < numberOfArrayRecords; x++) {
      model.items.push({
        string: "Test",
        number: 4,
        anotherObject: {
          items: [{
            id: 4,
            name: "Test"
          }, {
            id: 7,
            name: "Test2"
          }]
        }
      });
   
    }
};
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
ko.mapping
viewmodel = ko.mapping.fromJS(model);
pending…
ko.viewmodel
viewmodel = ko.viewmodel.fromModel(model);
pending…
knockout.wrap
viewmodel = ko.wrap.fromJS(model);
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment