ko.viewmodel vs ko.mapping vs knockout.wrap vs custom (v20)

Revision 20 of this benchmark created on


Description

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

Added custom mapper code

Preparation HTML

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

Setup

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"}] 
            }      
        });   
            
    }

Test runner

Ready to run.

Testing in
TestOps/sec
ko.mapping
viewmodel = ko.mapping.fromJS(model);
ready
ko.viewmodel
viewmodel = ko.viewmodel.fromModel(model);
ready
knockout.wrap
viewmodel = ko.wrap.fromJS(model);
ready
custom mapper
viewmodel = ko.mapper.fromJS(model);
ready

Revisions

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