UNPKG

transactional

Version:

Reactive objects with transactional updates and automatic serialization

246 lines (194 loc) 7.16 kB
// Optimized Model.set functions //--------------------------------- /* Does two main things: 1) Invoke model-specific constructor for attributes cloning. It improves performance on large model updates. 2) Invoke attribute-specific comparison function. Improves performance for everything, especially nested stuff. attrSpec is required to provide two methods: transform( value, options, model, name ) -> value to transform value before assignment isChanged( value1, value2 ) -> bool to detect whenever attribute must be assigned and counted as changed Model is required to implement Attributes constructor for attributes cloning. */ // Special case set: used from model's native properties. // Single attribute change, no options, _no_ _nested_ _changes_ detection on deep update. // 1) Code is stripped for this special case // 2) attribute-specific transform function invoked internally var _ = require( 'underscore' ), Events = require( './backbone+' ).Events, error = require( './errors' ), trigger2 = Events.trigger2, trigger3 = Events.trigger3; module.exports = { isChanged : genericIsChanged, setSingleAttr : setSingleAttr, setAttrs : setAttrs, transaction : transaction, transform : applyTransform, __begin : __begin, __commit : __commit }; function genericIsChanged( a, b ){ return !( a === b || ( a && b && typeof a == 'object' && typeof b == 'object' && _.isEqual( a, b ) ) ); } function setSingleAttr( model, key, value, attrSpec ){ 'use strict'; var changing = model._changing, current = model.attributes; model._changing = true; if( !changing ){ model._previousAttributes = new model.Attributes( current ); } if( model._changed ) model._changed = null; var options = {}, prevValue = current[ key ], val = attrSpec.transform( value, options, model, key ); current[ key ] = val; if( attrSpec.isChanged( prevValue, val ) ){ model._pending = options; trigger3( model, 'change:' + key, model, val, options ); } if( changing ){ return model; } while( model._pending ){ options = model._pending; model._pending = false; model._changeToken = {}; trigger2( model, 'change', model, options ); } model._pending = false; model._changing = false; return model; } // call a_fun with a_args inside of set transaction. // model.set inside of a_fun will trigger change:attr // but only single 'change' will be triggered at the end of transaction // transactions can be nested function transaction( a_fun, context, args ){ var notChanging = !this._changing, options = {}; this._changing = true; if( notChanging ){ this._previousAttributes = new this.Attributes( this.attributes ); } if( this._changed ) this._changed = null; this.__begin(); var res = a_fun.apply( context || this, args ); this.__commit(); if( notChanging ){ while( this._pending ){ options = this._pending; this._pending = false; this._changeToken = {}; trigger2( this, 'change', this, options ); } this._pending = false; this._changing = false; } return res; } // General case set: used for multiple and nested model/collection attributes. // Does _not_ invoke attribute transform! It must be done at the the top level, // due to the problems with current nested changes detection algorithm. See 'setAttrs' function below. function bbSetAttrs( model, attrs, opts ){ 'use strict'; var options = opts || {}; // Extract attributes and options. var unset = options.unset, silent = options.silent, changes = [], changing = model._changing, current = model.attributes, attrSpecs = model.__attributes; model._changing = true; if( !changing ){ model._previousAttributes = new model.Attributes( current ); } if( model._changed ) model._changed = null; // For each `set` attribute, update or delete the current value. // Todo: optimize for complete attrs set. Iterate through attributes names array, // or (may be better) create precompiled loop unrolled forEach, extracting specs // and values. // Beware of single attr update with options. Need deep refactoring to remove penalty. for( var attr in attrs ){ var attrSpec = attrSpecs[ attr ], isChanged = attrSpec ? attrSpec.isChanged : genericIsChanged, val = unset ? undefined : attrs[ attr ]; if( isChanged( current[ attr ], val ) ){ changes.push( attr ); } current[ attr ] = val; } // Trigger all relevant attribute changes. if( !silent ){ if( changes.length ){ model._pending = options; } for( var i = 0, l = changes.length; i < l; i++ ){ attr = changes[ i ]; trigger3( model, 'change:' + attr, model, current[ attr ], options ); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. if( changing ){ return model; } if( !silent ){ while( model._pending ){ options = model._pending; model._pending = false; model._changeToken = {}; trigger2( model, 'change', model, options ); } } model._pending = false; model._changing = false; return model; } // Optimized Backbone Core functions // ================================= // Deep set model attributes, catching nested attributes changes function setAttrs( model, attrs, options ){ model.__begin(); applyTransform( model, attrs, model.__attributes, options ); model.__commit( attrs, options ); return model; } // transform attributes hash function applyTransform( model, attrs, attrSpecs, options ){ for( var name in attrs ){ var attrSpec = attrSpecs[ name ], value = attrs[ name ]; if( attrSpec ){ attrs[ name ] = attrSpec.transform( value, options, model, name ); } else{ error.unknownAttribute( model, name, value ); } } } function __begin(){ this.__duringSet++ || ( this.__nestedChanges = {} ); } function __commit( a_attrs, options ){ var attrs = a_attrs; if( !--this.__duringSet ){ var nestedChanges = this.__nestedChanges, attributes = this.attributes; attrs || ( attrs = {} ); // Catch nested changes. for( var name in nestedChanges ){ var value = name in attrs ? attrs[ name ] : attrs[ name ] = nestedChanges[ name ]; if( value === attributes[ name ] ){ // patch attributes to force change:name event attributes[ name ] = null; } } this.__nestedChanges = {}; } if( attrs ){ bbSetAttrs( this, attrs, options ); } }