ko.viewmodel vs ko.mapping vs knockout.wrap vs custom

JavaScript performance comparison

Revision 20 of this test case created

Info

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

Added custom mapper code

Preparation code

<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/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() {
    ko.mapper = {
        isFunction: function(functionToCheck) {
             var getType = {};
            return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
        },
        fromJS: function (raw, mappingOptions, target) {
            var self = this;
            mappingOptions = mappingOptions || {};
            target = target || {};
            for (var property in raw) {
   
                if (raw[property] instanceof Array) {
                    var createItem = function(options) { return self.mapProperty(options); };
   
                    if (mappingOptions[property]) {
                        createItem = mappingOptions[property].create;
                    }
   
                    var arrayToSet = raw[property].map(function(item) {
                        return createItem({ data: item, property: property, mapping: mappingOptions });
                    });
                   
                    if (this.isFunction(target[property])) {
                        target[property](arrayToSet);
                    } else {
                        target[property] = ko.observableArray(arrayToSet);
                    }
   
                } else {
                    this.setProperty(target, property, {data: raw[property], property: property, mapping: mappingOptions });
                }
            }
            return target;
        },
        setProperty: function (target, property, options) {
            if (options.mapping[options.property]) {
                target[property] = options.mapping[options.property].create({ data: options.data });
                return;
            }
   
            if (options.data != null && typeof options.data === 'object') {
                var obj = target[property] || {};
                target[property] = this.fromJS(options.data, options.mapping, obj);
            } else {
                if (this.isFunction(target[property])) {
                    target[property](options.data);
                } else {
                    target[property] = ko.observable(options.data);
                }
            }
        },
        mapProperty: function (options) {
            if (options.mapping[options.property]) {
                return options.mapping[options.property].create({data: options.data});
            }
   
            if (options.data != null && typeof options.data === 'object') {
                return this.fromJS(options.data, options.mapping, {});
            } else {
                return ko.observable(options.data);
            }
   
        }
    };
    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…
custom mapper
viewmodel = ko.mapper.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