mobservable
Advanced tools
Comparing version 0.5.2 to 0.5.3
@@ -0,1 +1,3 @@ | ||
MOBservable: library to create reactive data structures and functions (and React components that act as observer) | ||
#mobservable makes #frp easy; observe computations instead of streams | ||
@@ -2,0 +4,0 @@ |
@@ -74,2 +74,3 @@ /** | ||
delete descriptor.writable; | ||
descriptor.configurable = true; | ||
descriptor.get = function () { | ||
@@ -86,3 +87,3 @@ var observable = this.key = mobservable.mobservableStatic.computed(baseValue, this); | ||
Object.defineProperty(target, key, { | ||
configurable: false, enumberable: true, | ||
configurable: true, enumberable: true, | ||
get: function () { | ||
@@ -89,0 +90,0 @@ mobservable.mobservableStatic.props(this, key, undefined); |
@@ -1,1 +0,1 @@ | ||
var __extends=this.__extends||function(a,b){function c(){this.constructor=a}for(var d in b)b.hasOwnProperty(d)&&(a[d]=b[d]);c.prototype=b.prototype,a.prototype=new c},mobservable;!function(a){function b(b,c){return Array.isArray(b)?new m(b):"function"==typeof b?a.mobservableStatic.computed(b,c):a.mobservableStatic.primitive(b)}function c(a,b){if(!b.length)return[a,[]];if(!a.length)return[[],b];for(var c=[],d=[],e=0,f=0,g=a.length,h=!1,i=0,j=0,k=b.length,l=!1,m=!1;!m&&!h;){if(!l){if(g>e&&k>i&&a[e]===b[i]){if(e++,i++,e===g&&i===k)return[c,d];continue}f=e,j=i,l=!0}j+=1,f+=1,j>=k&&(m=!0),f>=g&&(h=!0),h||a[f]!==b[i]?m||b[j]!==a[e]||(d.push.apply(d,b.slice(i,j)),i=j+1,e++,l=!1):(c.push.apply(c,a.slice(e,f)),e=f+1,i++,l=!1)}return c.push.apply(c,a.slice(e)),d.push.apply(d,b.slice(i)),[c,d]}function d(a){console&&console.warn("[WARNING:mobservable] "+a)}function e(a){var b=!1;return function(){return b?void 0:(b=!0,a.apply(this,arguments))}}function f(){}a.mobservableStatic=function(a,c){return b(a,c)},a.mobservableStatic.value=b,a.mobservableStatic.primitive=a.mobservableStatic.reference=function(a){return new h(a).createGetterSetter()},a.mobservableStatic.computed=function(a,b){return new i(a,b).createGetterSetter()},a.mobservableStatic.expr=function(a,b){if(0===k.trackingStack.length)throw new Error("mobservable.expr can only be used inside a computed observable. Probably mobservable.computed should be used instead of .expr");return new i(a,b).get()},a.mobservableStatic.sideEffect=function(b,c){return a.mobservableStatic.computed(b,c).observe(f)},a.mobservableStatic.array=function(a){return new m(a)},a.mobservableStatic.props=function p(b,p,c){switch(arguments.length){case 0:throw new Error("Not enough arguments");case 1:return a.mobservableStatic.props(b,b);case 2:for(var d in p)a.mobservableStatic.props(b,d,p[d]);break;case 3:var e=Array.isArray(c),f=a.mobservableStatic.value(c,b);Object.defineProperty(b,p,{get:e?function(){return f}:f,set:e?function(a){f.replace(a)}:f,enumerable:!0,configurable:!1})}return b},a.mobservableStatic.observable=function(b,c,d){var e=d?d.value:null;"function"==typeof e?(delete d.value,delete d.writable,d.get=function(){var b=this.key=a.mobservableStatic.computed(e,this);return b},d.set=function(){throw console.trace(),new Error("It is not allowed to reassign observable functions")}):Object.defineProperty(b,c,{configurable:!1,enumberable:!0,get:function(){return a.mobservableStatic.props(this,c,void 0),this[c]},set:function(b){a.mobservableStatic.props(this,c,b)}})},a.mobservableStatic.toPlainValue=function q(a){if(a){if(a instanceof Array)return a.slice();if(a instanceof h)return a.get();if("function"==typeof a&&a.impl){if(a.impl instanceof h)return a();if(a.impl instanceof m)return a().slice()}else if("object"==typeof a){var b={};for(var c in a)b[c]=q(a[c]);return b}}return a},a.mobservableStatic.observeProperty=function(b,c,f,g){if(void 0===g&&(g=!1),!b||!c||void 0===b[c])throw new Error("Object '"+b+"' has no property '"+c+"'.");if(!f||"function"!=typeof f)throw new Error("Third argument to mobservable.observeProperty should be a function");var j=b[c];if(j instanceof h||j instanceof m)return j.observe(f,g);if(j.impl&&(j.impl instanceof h||j instanceof m))return j.impl.observe(f,g);var k=new i(function(){return b[c]},b),l=k.observe(f,g);return a.mobservableStatic.debugLevel&&0===k.dependencyState.observing.length&&d("mobservable.observeProperty: property '"+c+"' of '"+b+" doesn't seem to be observable. Did you define it as observable?"),e(function(){l(),k.dependencyState.dispose()})},a.mobservableStatic.watch=function r(a,b){var r=new j(a,b);return[r.value,function(){return r.dispose()}]},a.mobservableStatic.batch=function(a){return o.batch(a)},a.mobservableStatic.debugLevel=0;var g,h=function(){function a(a){this._value=a,this.changeEvent=new n,this.dependencyState=new k(this)}return a.prototype.set=function(a){if(a!==this._value){var b=this._value;this.dependencyState.markStale(),this._value=a,this.dependencyState.markReady(!0),this.changeEvent.emit(a,b)}},a.prototype.get=function(){return this.dependencyState.notifyObserved(),this._value},a.prototype.observe=function(a,b){var c=this;void 0===b&&(b=!1),this.dependencyState.setRefCount(1),b&&a(this.get(),void 0);var d=this.changeEvent.on(a);return e(function(){c.dependencyState.setRefCount(-1),d()})},a.prototype.createGetterSetter=function(){var a=this,b=this,c=function(a){return arguments.length>0?void b.set(a):b.get()};return c.observe=function(b,c){return a.observe(b,c)},c.impl=this,c.toString=function(){return a.toString()},c},a.prototype.toString=function(){return"Observable["+this._value+"]"},a}(),i=function(b){function c(a,c){if(b.call(this,void 0),this.func=a,this.scope=c,this.isComputing=!1,this.hasError=!1,"function"!=typeof a)throw new Error("ComputedObservable requires a function")}return __extends(c,b),c.prototype.get=function(){if(this.isComputing)throw new Error("Cycle detected");var b=this.dependencyState;if(b.isSleeping?k.trackingStack.length>0?(b.wakeUp(),b.notifyObserved()):this.compute():b.notifyObserved(),b.hasCycle)throw new Error("Cycle detected");if(this.hasError)throw a.mobservableStatic.debugLevel&&(console.trace(),d(this+": rethrowing caught exception to observer: "+this._value+(this._value.cause||""))),this._value;return this._value},c.prototype.set=function(a){throw new Error(this.toString()+": A computed observable does not accept new values!")},c.prototype.compute=function(){var a;try{if(this.isComputing)throw new Error("Cycle detected");this.isComputing=!0,a=this.func.call(this.scope),this.hasError=!1}catch(b){this.hasError=!0,console.error(this+"Caught error during computation: ",b),b instanceof Error?a=b:(a=new Error("MobservableComputationError"),a.cause=b)}if(this.isComputing=!1,a!==this._value){var c=this._value;return this._value=a,this.changeEvent.emit(a,c),!0}return!1},c.prototype.toString=function(){return"ComputedObservable["+this.func.toString()+"]"},c}(h),j=function(){function a(a,b){this.expr=a,this.onInvalidate=b,this.dependencyState=new k(this),this.didEvaluate=!1,this.dependencyState.computeNextState()}return a.prototype.compute=function(){return this.didEvaluate?(this.dispose(),this.onInvalidate()):(this.didEvaluate=!0,this.value=this.expr()),!1},a.prototype.dispose=function(){this.dependencyState.dispose()},a}();!function(a){a[a.STALE=0]="STALE",a[a.PENDING=1]="PENDING",a[a.READY=2]="READY"}(g||(g={}));var k=function(){function b(a){this.owner=a,this.state=g.READY,this.isSleeping=!0,this.hasCycle=!1,this.observing=[],this.prevObserving=null,this.observers=[],this.dependencyChangeCount=0,this.dependencyStaleCount=0,this.isDisposed=!1,this.externalRefenceCount=0,this.isComputed=void 0!==a.compute}return b.prototype.setRefCount=function(a){var b=this.externalRefenceCount+=a;0===b?this.tryToSleep():b===a&&this.wakeUp()},b.prototype.addObserver=function(a){this.observers[this.observers.length]=a},b.prototype.removeObserver=function(a){var b=this.observers,c=b.indexOf(a);-1!==c&&(b.splice(c,1),0===b.length&&this.tryToSleep())},b.prototype.markStale=function(){this.state===g.READY&&(this.state=g.STALE,this.notifyObservers())},b.prototype.markReady=function(a){this.state!==g.READY&&(this.state=g.READY,this.notifyObservers(a))},b.prototype.notifyObservers=function(a){void 0===a&&(a=!1);for(var b=this.observers.slice(),c=b.length,d=0;c>d;d++)b[d].notifyStateChange(this,a)},b.prototype.tryToSleep=function(){if(!this.isSleeping&&this.isComputed&&0===this.observers.length&&0===this.externalRefenceCount){for(var a=0,b=this.observing.length;b>a;a++)this.observing[a].removeObserver(this);this.observing=[],this.isSleeping=!0}},b.prototype.wakeUp=function(){this.isSleeping&&this.isComputed&&(this.isSleeping=!1,this.state=g.PENDING,this.computeNextState())},b.prototype.notifyStateChange=function(a,b){var c=this;a.state===g.STALE?1===++this.dependencyStaleCount&&this.markStale():(b&&(this.dependencyChangeCount+=1),0===--this.dependencyStaleCount&&(this.state=g.PENDING,o.schedule(function(){c.dependencyChangeCount>0?c.computeNextState():c.markReady(!1),c.dependencyChangeCount=0})))},b.prototype.computeNextState=function(){this.trackDependencies();var a=this.owner.compute();this.bindDependencies(),this.markReady(a)},b.prototype.trackDependencies=function(){this.prevObserving=this.observing,b.trackingStack[b.trackingStack.length]=[]},b.prototype.bindDependencies=function(){this.observing=b.trackingStack.pop(),this.isComputed&&0===this.observing.length&&a.mobservableStatic.debugLevel>1&&!this.isDisposed&&(console.trace(),d("You have created a function that doesn't observe any values, did you forget to make its dependencies observable?"));var e=c(this.observing,this.prevObserving),f=e[0],g=e[1];this.prevObserving=null;for(var h=0,i=g.length;i>h;h++)g[h].removeObserver(this);this.hasCycle=!1;for(var h=0,i=f.length;i>h;h++)this.isComputed&&f[h].findCycle(this)?(this.hasCycle=!0,this.observing.splice(this.observing.indexOf(f[h]),1),f[h].hasCycle=!0):f[h].addObserver(this)},b.prototype.notifyObserved=function(){var a=b.trackingStack,c=a.length;if(c>0){var d=a[c-1],e=d.length;d[e-1]!==this&&d[e-2]!==this&&(d[e]=this)}},b.prototype.findCycle=function(a){var b=this.observing;if(-1!==b.indexOf(a))return!0;for(var c=b.length,d=0;c>d;d++)if(b[d].findCycle(a))return!0;return!1},b.prototype.dispose=function(){if(this.observers.length)throw new Error("Cannot dispose DNode; it is still being observed");if(this.observing)for(var a=this.observing.length,b=0;a>b;b++)this.observing[b].removeObserver(this);this.observing=null,this.isDisposed=!0},b.trackingStack=[],b}(),l=function(){function a(){}return a}();l.prototype=[];var m=function(b){function c(a){b.call(this),Object.defineProperties(this,{dependencyState:{enumerable:!1,value:new k(this)},_values:{enumerable:!1,value:a?a.slice():[]},changeEvent:{enumerable:!1,value:new n}}),a&&a.length&&this.updateLength(0,a.length)}return __extends(c,b),Object.defineProperty(c.prototype,"length",{get:function(){return this.dependencyState.notifyObserved(),this._values.length},set:function(a){if("number"!=typeof a||0>a)throw new Error("Out of range: "+a);var b=this._values.length;a!==b&&(a>b?this.spliceWithArray(b,0,new Array(a-b)):this.spliceWithArray(a,b-a))},enumerable:!0,configurable:!0}),c.prototype.updateLength=function(a,b){if(0>b)for(var d=a+b;a>d;d++)delete this[d];else if(b>0){a+b>c.OBSERVABLE_ARRAY_BUFFER_SIZE&&c.reserveArrayBuffer(a+b);for(var d=a,e=a+b;e>d;d++)Object.defineProperty(this,""+d,c.ENUMERABLE_PROPS[d])}},c.prototype.spliceWithArray=function(a,b,c){var d=this._values.length;if(!(void 0!==c&&0!==c.length||0!==b&&0!==d))return[];void 0===a?a=0:a>d?a=d:0>a&&(a=Math.max(0,d+a)),b=1===arguments.length?d-a:void 0===b||null===b?0:Math.max(0,Math.min(b,d-a)),void 0===c&&(c=[]);var e=c.length-b,f=(g=this._values).splice.apply(g,[a,b].concat(c));return this.updateLength(d,e),this.notifySplice(a,f,c),f;var g},c.prototype.notifyChildUpdate=function(a,b){this.notifyChanged(),this.changeEvent.emit({object:this,type:"update",index:a,oldValue:b})},c.prototype.notifySplice=function(a,b,c){(0!==b.length||0!==c.length)&&(this.notifyChanged(),this.changeEvent.emit({object:this,type:"splice",index:a,addedCount:c.length,removed:b}))},c.prototype.notifyChanged=function(){this.dependencyState.markStale(),this.dependencyState.markReady(!0)},c.prototype.observe=function(a,b){return void 0===b&&(b=!1),b&&a({object:this,type:"splice",index:0,addedCount:this._values.length,removed:[]}),this.changeEvent.on(a)},c.prototype.clear=function(){return this.splice(0)},c.prototype.replace=function(a){return this.spliceWithArray(0,this._values.length,a)},c.prototype.values=function(){return this.dependencyState.notifyObserved(),this._values.slice()},c.prototype.toJSON=function(){return this.dependencyState.notifyObserved(),this._values.slice()},c.prototype.clone=function(){return this.dependencyState.notifyObserved(),new c(this._values)},c.prototype.find=function(a,b,c){void 0===c&&(c=0),this.dependencyState.notifyObserved();for(var d=this._values,e=d.length,f=c;e>f;f++)if(a.call(b,d[f],f,this))return d[f];return null},c.prototype.splice=function(a,b){for(var c=[],d=2;d<arguments.length;d++)c[d-2]=arguments[d];switch(this.sideEffectWarning("splice"),arguments.length){case 0:return[];case 1:return this.spliceWithArray(a);case 2:return this.spliceWithArray(a,b)}return this.spliceWithArray(a,b,c)},c.prototype.push=function(){for(var a=[],b=0;b<arguments.length;b++)a[b-0]=arguments[b];return this.sideEffectWarning("push"),this.spliceWithArray(this._values.length,0,a),this._values.length},c.prototype.pop=function(){return this.sideEffectWarning("pop"),this.splice(Math.max(this._values.length-1,0),1)[0]},c.prototype.shift=function(){return this.sideEffectWarning("shift"),this.splice(0,1)[0]},c.prototype.unshift=function(){for(var a=[],b=0;b<arguments.length;b++)a[b-0]=arguments[b];return this.sideEffectWarning("unshift"),this.spliceWithArray(0,0,a),this._values.length},c.prototype.reverse=function(){return this.sideEffectWarning("reverse"),this.replace(this._values.reverse())},c.prototype.sort=function(a){return this.sideEffectWarning("sort"),this.replace(this._values.sort.apply(this._values,arguments))},c.prototype.remove=function(a){this.sideEffectWarning("remove");var b=this._values.indexOf(a);return b>-1?(this.splice(b,1),!0):!1},c.prototype.toString=function(){return this.wrapReadFunction("toString",arguments)},c.prototype.toLocaleString=function(){return this.wrapReadFunction("toLocaleString",arguments)},c.prototype.concat=function(){return this.wrapReadFunction("concat",arguments)},c.prototype.join=function(a){return this.wrapReadFunction("join",arguments)},c.prototype.slice=function(a,b){return this.wrapReadFunction("slice",arguments)},c.prototype.indexOf=function(a,b){return this.wrapReadFunction("indexOf",arguments)},c.prototype.lastIndexOf=function(a,b){return this.wrapReadFunction("lastIndexOf",arguments)},c.prototype.every=function(a,b){return this.wrapReadFunction("every",arguments)},c.prototype.some=function(a,b){return this.wrapReadFunction("some",arguments)},c.prototype.forEach=function(a,b){return this.wrapReadFunction("forEach",arguments)},c.prototype.map=function(a,b){return this.wrapReadFunction("map",arguments)},c.prototype.filter=function(a,b){return this.wrapReadFunction("filter",arguments)},c.prototype.reduce=function(a,b){return this.wrapReadFunction("reduce",arguments)},c.prototype.reduceRight=function(a,b){return this.wrapReadFunction("reduceRight",arguments)},c.prototype.wrapReadFunction=function(a,b){var d=Array.prototype[a];return(c.prototype[a]=function(){return this.dependencyState.notifyObserved(),d.apply(this._values,arguments)}).apply(this,b)},c.prototype.sideEffectWarning=function(b){a.mobservableStatic.debugLevel>0&&k.trackingStack.length>0&&d("[Mobservable.Array] The method array."+b+" should probably not be used inside observable functions since it has side-effects")},c.createArrayBufferItem=function(a){var b={enumerable:!1,configurable:!1,set:function(b){if(a<this._values.length){var c=this._values[a];c!==b&&(this._values[a]=b,this.notifyChildUpdate(a,c))}else{if(a!==this._values.length)throw new Error("ObservableArray: Index out of bounds, "+a+" is larger than "+this.values.length);this.push(b)}},get:function(){return a<this._values.length?(this.dependencyState.notifyObserved(),this._values[a]):void 0}};Object.defineProperty(c.prototype,""+a,b),b.enumerable=!0,b.configurable=!0,c.ENUMERABLE_PROPS[a]=b},c.reserveArrayBuffer=function(a){for(var b=c.OBSERVABLE_ARRAY_BUFFER_SIZE;a>b;b++)c.createArrayBufferItem(b);c.OBSERVABLE_ARRAY_BUFFER_SIZE=a},c.OBSERVABLE_ARRAY_BUFFER_SIZE=0,c.ENUMERABLE_PROPS=[],c}(l);m.reserveArrayBuffer(1e3);var n=function(){function a(){this.listeners=[]}return a.prototype.emit=function(){var a=this.listeners.slice(),b=a.length;switch(arguments.length){case 0:for(var c=0;b>c;c++)a[c]();break;case 1:for(var d=arguments[0],c=0;b>c;c++)a[c](d);break;default:for(var c=0;b>c;c++)a[c].apply(null,arguments)}},a.prototype.on=function(a){var b=this;return this.listeners.push(a),e(function(){var c=b.listeners.indexOf(a);-1!==c&&b.listeners.splice(c,1)})},a.prototype.once=function(a){var b=this.on(function(){b(),a.apply(this,arguments)});return b},a}();a.mobservableStatic.SimpleEventEmitter=n;var o=function(){function a(){}return a.schedule=function(b){a.inBatch<1?b():a.tasks[a.tasks.length]=b},a.runPostBatchActions=function(){for(var b=0;a.tasks.length;)try{for(;b<a.tasks.length;b++)a.tasks[b]();a.tasks=[]}catch(c){console.error("Failed to run scheduled action, the action has been dropped from the queue: "+c,c),a.tasks.splice(0,b+1)}},a.batch=function(b){a.inBatch+=1;try{return b()}finally{0===--a.inBatch&&(a.inBatch+=1,a.runPostBatchActions(),a.inBatch-=1)}},a.inBatch=0,a.tasks=[],a}();a.mobservableStatic.ObserverMixin={componentWillMount:function(){var b=this.render;this.render=function(){var c=this;this._watchDisposer&&this._watchDisposer();var e=a.mobservableStatic.watch(function(){return b.call(c)},function(){c.isMounted()?c.forceUpdate():a.mobservableStatic.debugLevel&&d("Rendering was triggered for unmounted component. Please check the lifecycle of the components")}),f=e[0],g=e[1];return this._watchDisposer=g,f}},componentWillUnmount:function(){this._watchDisposer&&this._watchDisposer()},shouldComponentUpdate:function(a,b){if(this.state!==b)return!0;var c,d=Object.keys(this.props);if(d.length!==Object.keys(a).length)return!0;for(var e=d.length-1;c=d[e];e--)if(a[c]!==this.props[c])return!0;return!1}},a.mobservableStatic.ObservingComponent=function(b){var c=b.componentWillMount,d=b.componentWillUnmount;return b.prototype.componentWillMount=function(){return a.mobservableStatic.ObserverMixin.componentWillMount.apply(this,arguments),c&&c.apply(this,arguments)},b.prototype.componentWillUnmount=function(){return a.mobservableStatic.ObserverMixin.componentWillUnmount.apply(this,arguments),d&&d.apply(this,arguments)},b.prototype.shouldComponentUpdate=a.mobservableStatic.ObserverMixin.shouldComponentUpdate,b},a.mobservableStatic.quickDiff=c,a.mobservableStatic.stackDepth=function(){return k.trackingStack.length}}(mobservable||(mobservable={})),function(a,b){"function"==typeof define&&define.amd?define("mobservable",[],function(){return b()}):"object"==typeof exports?module.exports=b():a.mobservable=b()}(this,function(){return mobservable.mobservableStatic}); | ||
var __extends=this.__extends||function(a,b){function c(){this.constructor=a}for(var d in b)b.hasOwnProperty(d)&&(a[d]=b[d]);c.prototype=b.prototype,a.prototype=new c},mobservable;!function(a){function b(b,c){return Array.isArray(b)?new m(b):"function"==typeof b?a.mobservableStatic.computed(b,c):a.mobservableStatic.primitive(b)}function c(a,b){if(!b.length)return[a,[]];if(!a.length)return[[],b];for(var c=[],d=[],e=0,f=0,g=a.length,h=!1,i=0,j=0,k=b.length,l=!1,m=!1;!m&&!h;){if(!l){if(g>e&&k>i&&a[e]===b[i]){if(e++,i++,e===g&&i===k)return[c,d];continue}f=e,j=i,l=!0}j+=1,f+=1,j>=k&&(m=!0),f>=g&&(h=!0),h||a[f]!==b[i]?m||b[j]!==a[e]||(d.push.apply(d,b.slice(i,j)),i=j+1,e++,l=!1):(c.push.apply(c,a.slice(e,f)),e=f+1,i++,l=!1)}return c.push.apply(c,a.slice(e)),d.push.apply(d,b.slice(i)),[c,d]}function d(a){console&&console.warn("[WARNING:mobservable] "+a)}function e(a){var b=!1;return function(){return b?void 0:(b=!0,a.apply(this,arguments))}}function f(){}a.mobservableStatic=function(a,c){return b(a,c)},a.mobservableStatic.value=b,a.mobservableStatic.primitive=a.mobservableStatic.reference=function(a){return new h(a).createGetterSetter()},a.mobservableStatic.computed=function(a,b){return new i(a,b).createGetterSetter()},a.mobservableStatic.expr=function(a,b){if(0===k.trackingStack.length)throw new Error("mobservable.expr can only be used inside a computed observable. Probably mobservable.computed should be used instead of .expr");return new i(a,b).get()},a.mobservableStatic.sideEffect=function(b,c){return a.mobservableStatic.computed(b,c).observe(f)},a.mobservableStatic.array=function(a){return new m(a)},a.mobservableStatic.props=function p(b,p,c){switch(arguments.length){case 0:throw new Error("Not enough arguments");case 1:return a.mobservableStatic.props(b,b);case 2:for(var d in p)a.mobservableStatic.props(b,d,p[d]);break;case 3:var e=Array.isArray(c),f=a.mobservableStatic.value(c,b);Object.defineProperty(b,p,{get:e?function(){return f}:f,set:e?function(a){f.replace(a)}:f,enumerable:!0,configurable:!1})}return b},a.mobservableStatic.observable=function(b,c,d){var e=d?d.value:null;"function"==typeof e?(delete d.value,delete d.writable,d.configurable=!0,d.get=function(){var b=this.key=a.mobservableStatic.computed(e,this);return b},d.set=function(){throw console.trace(),new Error("It is not allowed to reassign observable functions")}):Object.defineProperty(b,c,{configurable:!0,enumberable:!0,get:function(){return a.mobservableStatic.props(this,c,void 0),this[c]},set:function(b){a.mobservableStatic.props(this,c,b)}})},a.mobservableStatic.toPlainValue=function q(a){if(a){if(a instanceof Array)return a.slice();if(a instanceof h)return a.get();if("function"==typeof a&&a.impl){if(a.impl instanceof h)return a();if(a.impl instanceof m)return a().slice()}else if("object"==typeof a){var b={};for(var c in a)b[c]=q(a[c]);return b}}return a},a.mobservableStatic.observeProperty=function(b,c,f,g){if(void 0===g&&(g=!1),!b||!c||void 0===b[c])throw new Error("Object '"+b+"' has no property '"+c+"'.");if(!f||"function"!=typeof f)throw new Error("Third argument to mobservable.observeProperty should be a function");var j=b[c];if(j instanceof h||j instanceof m)return j.observe(f,g);if(j.impl&&(j.impl instanceof h||j instanceof m))return j.impl.observe(f,g);var k=new i(function(){return b[c]},b),l=k.observe(f,g);return a.mobservableStatic.debugLevel&&0===k.dependencyState.observing.length&&d("mobservable.observeProperty: property '"+c+"' of '"+b+" doesn't seem to be observable. Did you define it as observable?"),e(function(){l(),k.dependencyState.dispose()})},a.mobservableStatic.watch=function r(a,b){var r=new j(a,b);return[r.value,function(){return r.dispose()}]},a.mobservableStatic.batch=function(a){return o.batch(a)},a.mobservableStatic.debugLevel=0;var g,h=function(){function a(a){this._value=a,this.changeEvent=new n,this.dependencyState=new k(this)}return a.prototype.set=function(a){if(a!==this._value){var b=this._value;this.dependencyState.markStale(),this._value=a,this.dependencyState.markReady(!0),this.changeEvent.emit(a,b)}},a.prototype.get=function(){return this.dependencyState.notifyObserved(),this._value},a.prototype.observe=function(a,b){var c=this;void 0===b&&(b=!1),this.dependencyState.setRefCount(1),b&&a(this.get(),void 0);var d=this.changeEvent.on(a);return e(function(){c.dependencyState.setRefCount(-1),d()})},a.prototype.createGetterSetter=function(){var a=this,b=this,c=function(a){return arguments.length>0?void b.set(a):b.get()};return c.observe=function(b,c){return a.observe(b,c)},c.impl=this,c.toString=function(){return a.toString()},c},a.prototype.toString=function(){return"Observable["+this._value+"]"},a}(),i=function(b){function c(a,c){if(b.call(this,void 0),this.func=a,this.scope=c,this.isComputing=!1,this.hasError=!1,"function"!=typeof a)throw new Error("ComputedObservable requires a function")}return __extends(c,b),c.prototype.get=function(){if(this.isComputing)throw new Error("Cycle detected");var b=this.dependencyState;if(b.isSleeping?k.trackingStack.length>0?(b.wakeUp(),b.notifyObserved()):this.compute():b.notifyObserved(),b.hasCycle)throw new Error("Cycle detected");if(this.hasError)throw a.mobservableStatic.debugLevel&&(console.trace(),d(this+": rethrowing caught exception to observer: "+this._value+(this._value.cause||""))),this._value;return this._value},c.prototype.set=function(a){throw new Error(this.toString()+": A computed observable does not accept new values!")},c.prototype.compute=function(){var a;try{if(this.isComputing)throw new Error("Cycle detected");this.isComputing=!0,a=this.func.call(this.scope),this.hasError=!1}catch(b){this.hasError=!0,console.error(this+"Caught error during computation: ",b),b instanceof Error?a=b:(a=new Error("MobservableComputationError"),a.cause=b)}if(this.isComputing=!1,a!==this._value){var c=this._value;return this._value=a,this.changeEvent.emit(a,c),!0}return!1},c.prototype.toString=function(){return"ComputedObservable["+this.func.toString()+"]"},c}(h),j=function(){function a(a,b){this.expr=a,this.onInvalidate=b,this.dependencyState=new k(this),this.didEvaluate=!1,this.dependencyState.computeNextState()}return a.prototype.compute=function(){return this.didEvaluate?(this.dispose(),this.onInvalidate()):(this.didEvaluate=!0,this.value=this.expr()),!1},a.prototype.dispose=function(){this.dependencyState.dispose()},a}();!function(a){a[a.STALE=0]="STALE",a[a.PENDING=1]="PENDING",a[a.READY=2]="READY"}(g||(g={}));var k=function(){function b(a){this.owner=a,this.state=g.READY,this.isSleeping=!0,this.hasCycle=!1,this.observing=[],this.prevObserving=null,this.observers=[],this.dependencyChangeCount=0,this.dependencyStaleCount=0,this.isDisposed=!1,this.externalRefenceCount=0,this.isComputed=void 0!==a.compute}return b.prototype.setRefCount=function(a){var b=this.externalRefenceCount+=a;0===b?this.tryToSleep():b===a&&this.wakeUp()},b.prototype.addObserver=function(a){this.observers[this.observers.length]=a},b.prototype.removeObserver=function(a){var b=this.observers,c=b.indexOf(a);-1!==c&&(b.splice(c,1),0===b.length&&this.tryToSleep())},b.prototype.markStale=function(){this.state===g.READY&&(this.state=g.STALE,this.notifyObservers())},b.prototype.markReady=function(a){this.state!==g.READY&&(this.state=g.READY,this.notifyObservers(a))},b.prototype.notifyObservers=function(a){void 0===a&&(a=!1);for(var b=this.observers.slice(),c=b.length,d=0;c>d;d++)b[d].notifyStateChange(this,a)},b.prototype.tryToSleep=function(){if(!this.isSleeping&&this.isComputed&&0===this.observers.length&&0===this.externalRefenceCount){for(var a=0,b=this.observing.length;b>a;a++)this.observing[a].removeObserver(this);this.observing=[],this.isSleeping=!0}},b.prototype.wakeUp=function(){this.isSleeping&&this.isComputed&&(this.isSleeping=!1,this.state=g.PENDING,this.computeNextState())},b.prototype.notifyStateChange=function(a,b){var c=this;a.state===g.STALE?1===++this.dependencyStaleCount&&this.markStale():(b&&(this.dependencyChangeCount+=1),0===--this.dependencyStaleCount&&(this.state=g.PENDING,o.schedule(function(){c.dependencyChangeCount>0?c.computeNextState():c.markReady(!1),c.dependencyChangeCount=0})))},b.prototype.computeNextState=function(){this.trackDependencies();var a=this.owner.compute();this.bindDependencies(),this.markReady(a)},b.prototype.trackDependencies=function(){this.prevObserving=this.observing,b.trackingStack[b.trackingStack.length]=[]},b.prototype.bindDependencies=function(){this.observing=b.trackingStack.pop(),this.isComputed&&0===this.observing.length&&a.mobservableStatic.debugLevel>1&&!this.isDisposed&&(console.trace(),d("You have created a function that doesn't observe any values, did you forget to make its dependencies observable?"));var e=c(this.observing,this.prevObserving),f=e[0],g=e[1];this.prevObserving=null;for(var h=0,i=g.length;i>h;h++)g[h].removeObserver(this);this.hasCycle=!1;for(var h=0,i=f.length;i>h;h++)this.isComputed&&f[h].findCycle(this)?(this.hasCycle=!0,this.observing.splice(this.observing.indexOf(f[h]),1),f[h].hasCycle=!0):f[h].addObserver(this)},b.prototype.notifyObserved=function(){var a=b.trackingStack,c=a.length;if(c>0){var d=a[c-1],e=d.length;d[e-1]!==this&&d[e-2]!==this&&(d[e]=this)}},b.prototype.findCycle=function(a){var b=this.observing;if(-1!==b.indexOf(a))return!0;for(var c=b.length,d=0;c>d;d++)if(b[d].findCycle(a))return!0;return!1},b.prototype.dispose=function(){if(this.observers.length)throw new Error("Cannot dispose DNode; it is still being observed");if(this.observing)for(var a=this.observing.length,b=0;a>b;b++)this.observing[b].removeObserver(this);this.observing=null,this.isDisposed=!0},b.trackingStack=[],b}(),l=function(){function a(){}return a}();l.prototype=[];var m=function(b){function c(a){b.call(this),Object.defineProperties(this,{dependencyState:{enumerable:!1,value:new k(this)},_values:{enumerable:!1,value:a?a.slice():[]},changeEvent:{enumerable:!1,value:new n}}),a&&a.length&&this.updateLength(0,a.length)}return __extends(c,b),Object.defineProperty(c.prototype,"length",{get:function(){return this.dependencyState.notifyObserved(),this._values.length},set:function(a){if("number"!=typeof a||0>a)throw new Error("Out of range: "+a);var b=this._values.length;a!==b&&(a>b?this.spliceWithArray(b,0,new Array(a-b)):this.spliceWithArray(a,b-a))},enumerable:!0,configurable:!0}),c.prototype.updateLength=function(a,b){if(0>b)for(var d=a+b;a>d;d++)delete this[d];else if(b>0){a+b>c.OBSERVABLE_ARRAY_BUFFER_SIZE&&c.reserveArrayBuffer(a+b);for(var d=a,e=a+b;e>d;d++)Object.defineProperty(this,""+d,c.ENUMERABLE_PROPS[d])}},c.prototype.spliceWithArray=function(a,b,c){var d=this._values.length;if(!(void 0!==c&&0!==c.length||0!==b&&0!==d))return[];void 0===a?a=0:a>d?a=d:0>a&&(a=Math.max(0,d+a)),b=1===arguments.length?d-a:void 0===b||null===b?0:Math.max(0,Math.min(b,d-a)),void 0===c&&(c=[]);var e=c.length-b,f=(g=this._values).splice.apply(g,[a,b].concat(c));return this.updateLength(d,e),this.notifySplice(a,f,c),f;var g},c.prototype.notifyChildUpdate=function(a,b){this.notifyChanged(),this.changeEvent.emit({object:this,type:"update",index:a,oldValue:b})},c.prototype.notifySplice=function(a,b,c){(0!==b.length||0!==c.length)&&(this.notifyChanged(),this.changeEvent.emit({object:this,type:"splice",index:a,addedCount:c.length,removed:b}))},c.prototype.notifyChanged=function(){this.dependencyState.markStale(),this.dependencyState.markReady(!0)},c.prototype.observe=function(a,b){return void 0===b&&(b=!1),b&&a({object:this,type:"splice",index:0,addedCount:this._values.length,removed:[]}),this.changeEvent.on(a)},c.prototype.clear=function(){return this.splice(0)},c.prototype.replace=function(a){return this.spliceWithArray(0,this._values.length,a)},c.prototype.values=function(){return this.dependencyState.notifyObserved(),this._values.slice()},c.prototype.toJSON=function(){return this.dependencyState.notifyObserved(),this._values.slice()},c.prototype.clone=function(){return this.dependencyState.notifyObserved(),new c(this._values)},c.prototype.find=function(a,b,c){void 0===c&&(c=0),this.dependencyState.notifyObserved();for(var d=this._values,e=d.length,f=c;e>f;f++)if(a.call(b,d[f],f,this))return d[f];return null},c.prototype.splice=function(a,b){for(var c=[],d=2;d<arguments.length;d++)c[d-2]=arguments[d];switch(this.sideEffectWarning("splice"),arguments.length){case 0:return[];case 1:return this.spliceWithArray(a);case 2:return this.spliceWithArray(a,b)}return this.spliceWithArray(a,b,c)},c.prototype.push=function(){for(var a=[],b=0;b<arguments.length;b++)a[b-0]=arguments[b];return this.sideEffectWarning("push"),this.spliceWithArray(this._values.length,0,a),this._values.length},c.prototype.pop=function(){return this.sideEffectWarning("pop"),this.splice(Math.max(this._values.length-1,0),1)[0]},c.prototype.shift=function(){return this.sideEffectWarning("shift"),this.splice(0,1)[0]},c.prototype.unshift=function(){for(var a=[],b=0;b<arguments.length;b++)a[b-0]=arguments[b];return this.sideEffectWarning("unshift"),this.spliceWithArray(0,0,a),this._values.length},c.prototype.reverse=function(){return this.sideEffectWarning("reverse"),this.replace(this._values.reverse())},c.prototype.sort=function(a){return this.sideEffectWarning("sort"),this.replace(this._values.sort.apply(this._values,arguments))},c.prototype.remove=function(a){this.sideEffectWarning("remove");var b=this._values.indexOf(a);return b>-1?(this.splice(b,1),!0):!1},c.prototype.toString=function(){return this.wrapReadFunction("toString",arguments)},c.prototype.toLocaleString=function(){return this.wrapReadFunction("toLocaleString",arguments)},c.prototype.concat=function(){return this.wrapReadFunction("concat",arguments)},c.prototype.join=function(a){return this.wrapReadFunction("join",arguments)},c.prototype.slice=function(a,b){return this.wrapReadFunction("slice",arguments)},c.prototype.indexOf=function(a,b){return this.wrapReadFunction("indexOf",arguments)},c.prototype.lastIndexOf=function(a,b){return this.wrapReadFunction("lastIndexOf",arguments)},c.prototype.every=function(a,b){return this.wrapReadFunction("every",arguments)},c.prototype.some=function(a,b){return this.wrapReadFunction("some",arguments)},c.prototype.forEach=function(a,b){return this.wrapReadFunction("forEach",arguments)},c.prototype.map=function(a,b){return this.wrapReadFunction("map",arguments)},c.prototype.filter=function(a,b){return this.wrapReadFunction("filter",arguments)},c.prototype.reduce=function(a,b){return this.wrapReadFunction("reduce",arguments)},c.prototype.reduceRight=function(a,b){return this.wrapReadFunction("reduceRight",arguments)},c.prototype.wrapReadFunction=function(a,b){var d=Array.prototype[a];return(c.prototype[a]=function(){return this.dependencyState.notifyObserved(),d.apply(this._values,arguments)}).apply(this,b)},c.prototype.sideEffectWarning=function(b){a.mobservableStatic.debugLevel>0&&k.trackingStack.length>0&&d("[Mobservable.Array] The method array."+b+" should probably not be used inside observable functions since it has side-effects")},c.createArrayBufferItem=function(a){var b={enumerable:!1,configurable:!1,set:function(b){if(a<this._values.length){var c=this._values[a];c!==b&&(this._values[a]=b,this.notifyChildUpdate(a,c))}else{if(a!==this._values.length)throw new Error("ObservableArray: Index out of bounds, "+a+" is larger than "+this.values.length);this.push(b)}},get:function(){return a<this._values.length?(this.dependencyState.notifyObserved(),this._values[a]):void 0}};Object.defineProperty(c.prototype,""+a,b),b.enumerable=!0,b.configurable=!0,c.ENUMERABLE_PROPS[a]=b},c.reserveArrayBuffer=function(a){for(var b=c.OBSERVABLE_ARRAY_BUFFER_SIZE;a>b;b++)c.createArrayBufferItem(b);c.OBSERVABLE_ARRAY_BUFFER_SIZE=a},c.OBSERVABLE_ARRAY_BUFFER_SIZE=0,c.ENUMERABLE_PROPS=[],c}(l);m.reserveArrayBuffer(1e3);var n=function(){function a(){this.listeners=[]}return a.prototype.emit=function(){var a=this.listeners.slice(),b=a.length;switch(arguments.length){case 0:for(var c=0;b>c;c++)a[c]();break;case 1:for(var d=arguments[0],c=0;b>c;c++)a[c](d);break;default:for(var c=0;b>c;c++)a[c].apply(null,arguments)}},a.prototype.on=function(a){var b=this;return this.listeners.push(a),e(function(){var c=b.listeners.indexOf(a);-1!==c&&b.listeners.splice(c,1)})},a.prototype.once=function(a){var b=this.on(function(){b(),a.apply(this,arguments)});return b},a}();a.mobservableStatic.SimpleEventEmitter=n;var o=function(){function a(){}return a.schedule=function(b){a.inBatch<1?b():a.tasks[a.tasks.length]=b},a.runPostBatchActions=function(){for(var b=0;a.tasks.length;)try{for(;b<a.tasks.length;b++)a.tasks[b]();a.tasks=[]}catch(c){console.error("Failed to run scheduled action, the action has been dropped from the queue: "+c,c),a.tasks.splice(0,b+1)}},a.batch=function(b){a.inBatch+=1;try{return b()}finally{0===--a.inBatch&&(a.inBatch+=1,a.runPostBatchActions(),a.inBatch-=1)}},a.inBatch=0,a.tasks=[],a}();a.mobservableStatic.ObserverMixin={componentWillMount:function(){var b=this.render;this.render=function(){var c=this;this._watchDisposer&&this._watchDisposer();var e=a.mobservableStatic.watch(function(){return b.call(c)},function(){c.isMounted()?c.forceUpdate():a.mobservableStatic.debugLevel&&d("Rendering was triggered for unmounted component. Please check the lifecycle of the components")}),f=e[0],g=e[1];return this._watchDisposer=g,f}},componentWillUnmount:function(){this._watchDisposer&&this._watchDisposer()},shouldComponentUpdate:function(a,b){if(this.state!==b)return!0;var c,d=Object.keys(this.props);if(d.length!==Object.keys(a).length)return!0;for(var e=d.length-1;c=d[e];e--)if(a[c]!==this.props[c])return!0;return!1}},a.mobservableStatic.ObservingComponent=function(b){var c=b.componentWillMount,d=b.componentWillUnmount;return b.prototype.componentWillMount=function(){return a.mobservableStatic.ObserverMixin.componentWillMount.apply(this,arguments),c&&c.apply(this,arguments)},b.prototype.componentWillUnmount=function(){return a.mobservableStatic.ObserverMixin.componentWillUnmount.apply(this,arguments),d&&d.apply(this,arguments)},b.prototype.shouldComponentUpdate=a.mobservableStatic.ObserverMixin.shouldComponentUpdate,b},a.mobservableStatic.quickDiff=c,a.mobservableStatic.stackDepth=function(){return k.trackingStack.length}}(mobservable||(mobservable={})),function(a,b){"function"==typeof define&&define.amd?define("mobservable",[],function(){return b()}):"object"==typeof exports?module.exports=b():a.mobservable=b()}(this,function(){return mobservable.mobservableStatic}); |
88
frp.md
@@ -1,5 +0,85 @@ | ||
Why the equality operator might change the world of programming | ||
Pure Rendering? Thou shalt not use assignments | ||
In programming the equals character is used a lot ('='); it literally ties all our code together. JavaScript for example provides three different operators based on the equals character, '=', '==' and '===' . The first one denotes assignment, the other two compare values. Yet none of those express the mathematical meaning of equality. That is sad, because programming would be fundamentally more easy if this wasn't the case. | ||
Last week Guillermo Raunch wrote in an excellent [blog post](http://rauchg.com/2015/pure-ui/) that the UI should be a pure function of the application state. | ||
And he is right. Pure user interfaces are easy to reason about, to mock, to test. A pure UI can be described as | ||
UI = view(application_state) | ||
Where the view transform the data in something that would be put on the screen. This is also one of the core fundamentals of a user interface libraries such as React. | ||
However, once you start to program the above pattern, you will soon discover that the UI *isn't* a function of the application_state, it *was* a function of the application state. | ||
Now how did this happen? It's all the fault of the assignment operator. In mathematics, the equals sign (`=`) expresses an equation. The left-hand side is an isomorphic equivalence of the right hand sight. That means that every occurrence of `UI` can be read anywhere as `view(application_state)`, and if application_state morphs, so does the UI. | ||
Yet, in programming, the equal sign denotes assignment. Meaning; reduce the right hand site to a value and assign it to the left hand side. Superficially, this seems to be the same. But the problem is that with assignments the UI isn't an isomorph of the view anymore, its a one time copy! This means that if the application state changes, you have to carefully reapply the view function to the UI. | ||
This might not sound as a big deal, but actually it is. First, its awkward to state each time the application state changes that the UI should be updated. In event handlers like the one below it becomes painfully clear that our UI isn't a pure function of state*-over-time*: | ||
function onDeleteButtonClick (todoItem) { | ||
allTodos.remove(todoItem); | ||
redrawTodoList(); | ||
} | ||
Besides, creating a fresh view from the application state is [expensive](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/) for any decently sized (web) app. So in our event handlers we take guesses which parts of the UI needs an update, or we start attaching events to certain state updates and start listening to those events in our UI components. Bye bye pure function. Welcome, boilerplate filled spaghetti code. | ||
So first we define nice relationship between UI and state. Then it appears that this relationship is just a farce; it isn't maintained by the runtime and so you, as a developer have the honorable job to fix this relationship every time something changes. That doesn't sound like a healthy relationship! It sounds like you should have studied mediation! Can we fix this issue at the root? Can we fix the assignment operator? Can we just write: | ||
UI <= view(application_state) | ||
Where '<=' establishes a relationship between `UI` and the application of `view` to `application_state`. Note that the '<='' operator nicely mirrors the '=>' lambda operator. A function produces a value which you need to propagate carefully through your app, but true isomorphic equality just 'pulls' data towards you. | ||
Central though: user interface code will be of higher quality if we shift from one-time assignment operators to isomorphic relationships between data | ||
Sadly, we don't have such an operator, so the code we write is flooded with statements like "notify that function", "emit this event", "repaint that part of the UI", "now send this update to the server". The complexity and fragility of our apps would be largely reduced if we would no longer write stuff like that. | ||
Is it possible to have an isomorphic equality operator? Yes definitely. Spreadsheet applications like Microsoft Excel do understand correctly what the equality operator is all about. | ||
I strongly believe that it is for that specific reason why Excel is one of the most successful software products of all times. Because in spreadsheets, one can just state: | ||
C100 = SUM(B2:B99) / A1 | ||
In Excel the equals sign establishes a proper relationship, where C100 will follow any changes that happen in the B column or A1. Awesome, right? The solution to proper programming is in our midst already for decades! | ||
Functional Reactive Programming is the discipline that applies the principle of reacting to changing data structures to the programming field. However its approach is a bit cumbersome as it expresses relationships as streams. So basically, it is for people way smarter than me. But alas, the direction is good and it does the job. In existing FRP libraries like RxJS or Bacon our relationship would look a bit like: | ||
budgetStream = creditCardLimitStream.combine(reservedMoneyStream, (ccl, rm) => ccl — rm); | ||
So, can we leave all the fluff away and observe expressions instead of streams, like in Excel? Yes we can! The [MOBservable library](https://npmjs.org/package/MOBservable) can do precisely that trick for us. In MOBservable, the syntactical equivalent of 'value <= expr ' is 'value = mobservable(() => expr)'. This is a bit easier to grok, especially because all data dependencies inside expressions are managed automatically. Well, then show me the thing! Well OK, glad you asked, just open a terminal in an empty directory and copy+paste the following command to get started (assuming you have [nodejs](https://nodejs.org) installed: | ||
(yes ""| npm init) && npm install mobservable --save && node -e "global.mobservable=require('mobservable'); require('repl').start({ useGlobal: true});" | ||
There it is! Your JavaScript environment with MOBservable pre-loaded. Now just type (or copy-paste): | ||
var state = mobservable.props({ | ||
nrOfSeats : 500, | ||
reservations : [], | ||
seatsLeft : function() { return this.nrOfSetas - this.reservations.length; } | ||
}); | ||
var ui = mobservable(function() { | ||
return "<div>" + state.seatsLeft +"</div>"; | ||
}); | ||
ui.observe(console.log, true); | ||
state.reservations[0] = "Michel"; | ||
state.reservations.push("You?"); | ||
state.nrOfSeats = 750; | ||
Output: | ||
note: the output was atomic, each transition in the state results in only one view, there is no rendering where the seatsLeft doesn't match the amount of reservations | ||
second: just changes the nrOfsetas generates a new ui, although its value was used inside the view only indirectly (through seatsLeft) | ||
Did you see how it printed first '<div> 499 </div>', '<div> 498 </div>' and finally '<div> 748 </div>' automatically? That is because our view function was applied to the state automatically whenever a piece of state (that was actually used in the UI) changed! The `props` function created a reactive state for us and we defined the ui as pure function of that state. Finally we pushed any changes in the ui to the console. (That is needed because consoles are not reactive, so observing the ui creates a bridge there). Syntactically it is not perfect, but well, it works in any existing JavaScript environments out-of-the-box and that is worth something as well. And MOBserable does this all this automatically (and atomically) for objects, classes, arrays, functions, simple scalars... | ||
Interested in more? Take a look at a less contrived example like a real Todo app[TODO: link, add to todo app], or play on in the terminal. For those who are wondering how this relationship magic works.. thats for a next blog post. As is an introduction about how to combine these concepts with existing UI frameworks like React or jQuery. For now I have other relationships to take care of... ;-) | ||
Tl;dr: Applications development will become less error prone and more robust to changes if we reduce the amount of local state through our apps by introducing isomorphic equality. That is; an equality operator that stands the test of time. Applying transparent functional reactive programming will get us there. | ||
Oh, and if you are wondering whether you can apply the MOBservable library to your own project right now, the library is currently used in an enterprise scale app, to manage the complete state, back-end integration and especially the view of a data intensive React application (about ~30kloc) while tracking the relationships between the ui and thousands of objects and expressions. | ||
----- | ||
How a proper equality operator should change the art of programming | ||
In programming the equals character is used a lot ('='); it literally ties all our code together. JavaScript for example provides three different operators based on the equals character, '=', '==' and '===' . The first one denotes assignment, the other two compare values. Yet none of those express the mathematical concept of equality. That is sad, because programming would be fundamentally more easy if this wasn't the case. | ||
Let's take a look at the assignment operator. You can use it to assign values to a variable. For instance: | ||
@@ -9,5 +89,5 @@ | ||
Seems perfectly legit right? In mathematics this is nonsense. This ain't a valid equation. There is simply no number for which holds that it is one larger than itself (well, OK, except for infinity). | ||
equation equivalent isomorph | ||
In mathematics the equals sign means: the thing on the left equals the thing on the right; you can safely substitute the one for the other, everywhere you want. In programming, assignment means: Reduce the right hand side to a value and store it in the variable on the left hand side. For example: | ||
Seems perfectly legit right? However, This ain't a valid equation. There is simply no number for which holds that it is one larger than itself (well, OK, except for infinity). In mathematics the equals sign means: the thing on the left equals the thing on the right; you can safely substitute the one for the other, everywhere you want. In programming, assignment means: Reduce the right hand side to a value and store it in the variable on the left hand side. For example: | ||
@@ -14,0 +94,0 @@ budget = creditCardLimit - reservedMoney |
@@ -189,2 +189,3 @@ /** | ||
delete descriptor.writable; | ||
descriptor.configurable = true; | ||
descriptor.get = function() { | ||
@@ -200,3 +201,3 @@ var observable = this.key = mobservableStatic.computed(baseValue, this); | ||
Object.defineProperty(target, key, { | ||
configurable: false, enumberable:true, | ||
configurable: true, enumberable:true, | ||
get: function() { | ||
@@ -203,0 +204,0 @@ mobservableStatic.props(this, key, undefined); |
{ | ||
"name": "mobservable", | ||
"version": "0.5.2", | ||
"version": "0.5.3", | ||
"description": "Changes are coming! Small library for creating observable properties, arrays and functions", | ||
@@ -5,0 +5,0 @@ "main": "dist/mobservable.js", |
223
README.md
@@ -8,11 +8,13 @@ # MOBservable | ||
MOBservable is light-weight stand-alone library to create reactive primitives, functions, arrays and objects. | ||
MOBservable is light-weight standalone transparent reactive programming library to create Reactive primitives, functions, arrays and objects. | ||
Its goal is to make developers happy and productive, by removing boilerplate work such as invalidating derived data or managing event listeners. | ||
Its goal is to make developers happy and productive by removing boilerplate work such as invalidating derived data or managing event listeners. | ||
It makes sure data changes are automatically, atomically and synchronously propagated through your app without being obtrusive. | ||
MOBservable runs in any ES5 environment but features also some React addons. | ||
It is higly efficient and shines when managing large amounts of complex, cyclic, nested or computed data. | ||
MOBservable runs in any ES5 environment but features also some React add-ons. | ||
It is highly efficient and shines when managing large amounts of complex, cyclic, nested or computed data. | ||
Some links that may be interesting: | ||
* [Slack group](https://mobservable.slack.com) | ||
* [Blog post: combining React with MOBservable](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/) | ||
* [Blog post: combining React with MOBservable to create high performing and easily maintainable apps](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/) | ||
* [Examples](#examples) | ||
@@ -23,4 +25,6 @@ * [Design principles](#design-principles) | ||
# Examples | ||
* [TodoMVC in Mobservable + React](https://rawgit.com/mweststrate/todomvc/immutable-to-observable/examples/react-mobservable/index.html#/), ... and the [diff](https://github.com/mweststrate/todomvc/commit/2e30caeb8c690c914f92081ac01d12097a068a1e) of using observables with react instead of immutables. | ||
* [Fiddle demo: MOBservable + React: simple timer](https://jsfiddle.net/mweststrate/wgbe4guu/) | ||
@@ -30,7 +34,7 @@ * [Fiddle demo: MOBservable + React: shop](https://jsfiddle.net/mweststrate/46vL0phw) | ||
The source of all demo's can also be found in the [example](/example) folder. | ||
The source of all demos can also be found in the [example](/example) folder. | ||
## Example: Observable values and functions | ||
The core of `MOBservable` consists of observable values, functions that automatically recompute when an observed value changes, | ||
The core of `MOBservable` consists of observable values, i.e. functions that automatically recompute when an observed value changes, | ||
and the possibility to listen to changing values and updated computations. | ||
@@ -56,3 +60,3 @@ | ||
// calling an mobservable with a value acts as setter, | ||
// Calling an mobservable with a value acts as setter, | ||
// ...and automatically updates all computations in which it was used | ||
@@ -71,3 +75,3 @@ nrOfCatz(34); | ||
var Person = function(firstName, lastName) { | ||
// define the observable properties firstName, lastName and fullName on 'this'. | ||
// Define the observable properties firstName, lastName and fullName on 'this': | ||
mobservable.props(this, { | ||
@@ -84,7 +88,7 @@ firstName: firstName, | ||
// (computed) properties can be accessed like any other property: | ||
// (Computed) Properties can be accessed like any other property: | ||
console.log(jane.fullName); | ||
// prints: "Jan Dôh" | ||
// properties can be observed as well: | ||
// Properties can be observed as well: | ||
mobsevable.observeProperty(jane, "fullName", console.log); | ||
@@ -99,4 +103,3 @@ | ||
`mobservable` provides an observable array implementation (as ES7 polyfill), which is fully ES5 compliant, | ||
but which will notify dependent computations upon each change. | ||
`mobservable` provides an observable array implementation (as ES7 polyfill) which is fully ES5 compliant but which will notify dependent computations upon each change. | ||
@@ -106,6 +109,6 @@ ```javascript | ||
// create an array, that works by all means as a normal array, except that it is observable! | ||
// Create an array, that works by all means as a normal array, except that it is observable! | ||
var someNumbers = mobservable.value([1,2,3]); | ||
// a naive function that sums all the values | ||
// A naive function that sums all the values: | ||
var sum = mobservable.value(function() { | ||
@@ -128,3 +131,3 @@ for(var s = 0, i = 0; i < someNumbers.length; i++) | ||
For typescript users, `mobservable` ships with module typings and an `@observable` annotation with which class members can be marked as observable. | ||
For TypeScript users, `mobservable` ships with module typings and an `@observable` annotation with which class members can be marked as observable. | ||
@@ -167,3 +170,3 @@ ```typescript | ||
## Example: ObservingComponent for react components | ||
## Example: ObservingComponent for React components | ||
@@ -175,3 +178,3 @@ MOBservable ships with a mixin and class decorator that can be used to subscribe React components to observables automatically. | ||
var store = {}; | ||
// add observable properties to the store | ||
// Add observable properties to the store: | ||
mobservable.props(store, { | ||
@@ -215,2 +218,3 @@ timer: 0 // this could be an array, object, function as well.. | ||
# Design principles | ||
@@ -223,5 +227,5 @@ | ||
- The Model, View (and Contorller) of an app should be separated. | ||
- The Model, View (and Controller) of an app should be separated. | ||
Views should be loosely coupled to the UI, so that UI refactorings do not require changes of the data model. | ||
It should be possible to describe views on the data model as naturally as possible, as-if data does not change over time. | ||
It should be possible to describe views on the data model as naturally as possible, as if data does not change over time, or in other words: as a pure function of state. | ||
- Derived data should be re-calculated automatically and efficiently. | ||
@@ -232,7 +236,7 @@ It is the responsibility of MOBservable to prevent that views ever become stale. | ||
- Data should be mutable as this is close to the natural mental model of most kinds of data. | ||
<small>(despite some nice properties of immutable data, mutable data is easier to inspect, read, grok and especially more natural to program against. | ||
<small>Despite some nice properties of immutable data, mutable data is easier to inspect, read, grok and especially more natural to program _explicitly_ against. | ||
`markRead(email) { email.isRead = true; }` is more convenient to write than `markRead(email) { return { ...email, isRead : true }; }` or `markRead(email) { model.set('email', 'isRead', true); }`. | ||
Especially when email is somewhere deep in your model tree)</small> | ||
Especially when email is somewhere deep in your model tree.</small> | ||
- Subscriptions should be a breeze to manage, and managed automatically wherever possible. | ||
- MOBservable is only about the model data, not about querying, back-end communication etc (although observers are really useful there as well). | ||
- MOBservable is only about the model data, not about querying, back-end communication etc. (although observers are really useful there as well). | ||
@@ -243,12 +247,13 @@ ## Behavior | ||
* _synchronous_. Updates are processed synchronously, that is, the pseudo expressions `a = 3; b -> a * 2; a = 4; print(b); ` will always print `8`; `b` will never yield a stale value. | ||
* _atomic_. Computed values will postpone updates until all inputs are settled, to make sure no intermediate values are visible. That is, the expression `a = 3; b -> a * 2; c -> a * b; a = 4; print(c)` will always print `32` and no intermediate values like `24`. | ||
* _real time dependency detection_. Computed values only depend on values actually used in the last computation, for example, given: `a -> b > 5 ? c : b` the variable `c` will only cause a re-evaluation of `a` if `b > 5`. | ||
* _lazy_. Computed values will only be evaluated if they are actually being observed. So make sure computed functions are pure and side effect free; the library might not evaluate expressions as often as you thought it would. | ||
* _cycle detection_. Cycles in computations, like in `a -> 2 * b; b -> 2 * a;` will be deteced. | ||
* _error handling_. Exceptions that are raised during computations are propagated to consumers. | ||
* _synchronous_ - Updates are processed synchronously, that is, the pseudo expressions `a = 3; b -> a * 2; a = 4; print(b); ` will always print `8`; `b` will never yield a stale value. | ||
* _atomic_ - Computed values will postpone updates until all inputs are settled, to make sure no intermediate values are visible. That is, the expression `a = 3; b -> a * 2; c -> a * b; a = 4; print(c)` will always print `32` and no intermediate values like `24`. | ||
* _real time dependency detection_ - Computed values only depend on values actually used in the last computation, for example, given: `a -> b > 5 ? c : b` the variable `c` will only cause a re-evaluation of `a` if `b > 5`. | ||
* _lazy_ - Computed values will only be evaluated if they are actually being observed. So make sure computed functions are pure and side effect-free; the library might not evaluate expressions as often as you thought it would. | ||
* _cycle detection_ - Cycles in computations, like in `a -> 2 * b; b -> 2 * a;` will be detected. | ||
* _error handling_ - Exceptions that are raised during computations are propagated to consumers. | ||
# API Documentation | ||
[Typescript typings](https://github.com/mweststrate/MOBservable/blob/master/mobservable.d.ts) | ||
The [Typescript typings](https://github.com/mweststrate/MOBservable/blob/master/mobservable.d.ts) serve as offline API documentation. | ||
@@ -259,3 +264,3 @@ ## Creating observables | ||
Shorthand for `mobservable.value` | ||
Shorthand for `mobservable.value`. | ||
@@ -268,5 +273,6 @@ ### mobservable.value | ||
Function that creates an observable given a `value`. | ||
Creates an observable given a `value`. | ||
Depending on the type of the function, this function invokes `mobservable.array`, `mobservable.computed` or `mobservable.primitive`. | ||
See the examples above for usage patterns. The `scope` is only meaningful if a function is passed into this method. | ||
See the examples above for usage patterns. | ||
The `scope` is only meaningful if a function is passed into this method. | ||
@@ -277,5 +283,5 @@ ### mobservable.primitive | ||
Creates a new observable, initialzed with the given `value` that can change over time. | ||
The returned observable is a function, that without arguments acts as getter, and with arguments as setter. | ||
Furthermore its value can be observed using the `.observe` method, see `IObservableValue.observe`. | ||
Creates a new observable, initialized with the given `value` that can change over time. | ||
The returned observable is a function that without arguments acts as getter, and with arguments as setter. | ||
Furthermore, its value can be observed using the `.observe` method, see `IObservableValue.observe`. | ||
@@ -294,3 +300,3 @@ Example: | ||
Synonym for `mobservable.primitive`, since the equality of primitives is determined in the same way as references, namely by strict equality. | ||
(from version 0.6, see `mobservable.struct` if values need to be compared structuraly by using deep equality). | ||
(From version 0.6, see `mobservable.struct` if values need to be compared structurally by using deep equality.) | ||
@@ -301,5 +307,6 @@ ### mobservable.computed | ||
`computed` turns a function into an observable value. | ||
The provided `expr` should not have any arguments, but instead really on other observables that are in scope to determine its value. | ||
The latest value returned by `expr` determines the value of the observable. When one of the observables used in `expr` changes, `computed` will make sure that the function gets re-evaluated, and all updates are propogated to the children. | ||
Turns a function into an observable value. | ||
The provided `expr` should not have any arguments, but instead rely on other observables that are in scope to determine its value. | ||
The latest value returned by `expr` determines the value of the observable. | ||
When one of the observables used in `expr` changes, `computed` will make sure that the function gets re-evaluated, and all updates are propagated to the children. | ||
@@ -320,3 +327,5 @@ ```javascript | ||
`computed` will try to reduce the amount of re-evaluates of `expr` as much as possible. For that reason the function *should* be pure, that is: | ||
`computed` will try to reduce the amount of re-evaluates of `expr` as much as possible. | ||
For that reason the function *should* be pure, that is: | ||
* The result of `expr` should only be defined in terms of other observables, and not depend on any other state. | ||
@@ -328,5 +337,6 @@ * Your code shouldn't rely on any side-effects, triggered by `expr`; `expr` should be side-effect free. | ||
It is allowed to throw exceptions in an observed function. The thrown exceptions might only be detected late. | ||
The exception will be rethrown if somebody inspects the current value, and will be passed as first callback argument | ||
to all the listeners. | ||
It is allowed to throw exceptions in an observed function. | ||
The thrown exceptions might only be detected late. | ||
The exception will be re-thrown if somebody inspects the current value, and will be passed as first callback argument | ||
to all the listeners. | ||
@@ -369,9 +379,7 @@ ### mobservable.expr | ||
Constructs an array like, observable structure. An observable array is a thin abstraction over native arrays and adds observable properties. | ||
The most notable difference between built-in arrays is that these arrays cannot be sparse, that is, | ||
values assigned to an index larger than `length` are considered out-of-bounds and not oberved | ||
(nor any other property that is assigned to a non-numeric pr negative index). | ||
Constructs an array like, observable structure. | ||
An observable array is a thin abstraction over native arrays and adds observable properties. | ||
The most notable difference between built-in arrays is that these arrays cannot be sparse, i.e. values assigned to an index larger than `length` are considered out-of-bounds and not observed (nor any other property that is assigned to a non-numeric pr negative index). | ||
Furthermore, `Array.isArray(observableArray)` and `typeof observableArray === "array"` will yield `false` for observable arrays, | ||
but `observableArray instanceof Array` will return `true`. | ||
Furthermore, `Array.isArray(observableArray)` and `typeof observableArray === "array"` will yield `false` for observable arrays, but `observableArray instanceof Array` will return `true`. | ||
@@ -395,9 +403,9 @@ ```javascript | ||
* `observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda` Listen to changes in this array. The callback will receive arguments that express an array splice or array change, conform the [ES7 proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe) | ||
* `clear(): T[]` Remove all current entries from the array | ||
* `observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda` Listen to changes in this array. The callback will receive arguments that express an array splice or array change, conforming to [ES7 proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe) | ||
* `clear(): T[]` Remove all current entries from the array. | ||
* `replace(newItems:T[])` Replaces all existing entries in the array with new ones. | ||
* `values(): T[]` Returns a shallow, non-observable clone of the array, similar to `.slice` | ||
* `clone(): IObservableArray<T>` Create a new observable array containing the same values | ||
* `find(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T` Find implementation, basically the same as the ES7 Array.find proposal, but with added `fromIndex` parameter. | ||
* `remove(value:T):boolean` Remove a single item by value from the array. Returns true if the item was found and removed. | ||
* `values(): T[]` Returns a shallow, non-observable clone of the array, similar to `.slice`. | ||
* `clone(): IObservableArray<T>` Create a new observable array containing the same values. | ||
* `find(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T` Find implementation, basically the same as the ES7 Array.find proposal, but with added `fromIndex` parameter. | ||
* `remove(value:T):boolean` Remove a single item by value from the array. Returns true if the item was found and removed. | ||
@@ -413,6 +421,7 @@ ### mobservable.props | ||
Creates observable properties on the given `target` object. This function uses `mobservable.value` internally to create observables. | ||
Creating properties has as advantage that they are more convenient to use. See also [value versus props](#value-versus-props). | ||
The original `target`, with the added properties, is returned by this function. Functions used to created computed observables will automatically | ||
be bound to the correct `this`. | ||
Creates observable properties on the given `target` object. | ||
This function uses `mobservable.value` internally to create observables. | ||
Creating properties has as advantage that they are more convenient to use; see also [value versus props](#value-versus-props). | ||
The original `target`, with the added properties, is returned by this function. | ||
Functions used to created computed observables will automatically be bound to the correct `this`. | ||
@@ -432,6 +441,5 @@ ```javascript | ||
Note that observables created by `mobservable.props` do not expose an `.observe` method, | ||
to observe properties, see [`mobservable.observeProperty`](#mobservableobserveproperty) | ||
Note that observables created by `mobservable.props` do not expose an `.observe` method; to observe properties, see [`mobservable.observeProperty`](#mobservableobserveproperty). | ||
Other forms in which this function can be called: | ||
Other forms in which this function can be used: | ||
```javascript | ||
@@ -446,4 +454,5 @@ mobservable.props(order, "price", 3); // equivalent to mobservable.props(order, { price: 3 }); | ||
Typescript 1.5 introduces annotations. The `mobservable.observable` annotation can be used to mark class properties and functions as observable. | ||
This annotations basically wraps `mobservable.props`. Example: | ||
Typescript 1.5 introduces annotations. | ||
The `mobservable.observable` annotation can be used to mark class properties and functions as observable. | ||
This annotations basically wraps `mobservable.props`. Example: | ||
@@ -464,2 +473,6 @@ ```typescript | ||
``` | ||
Please note that adding the `@observable` annotations to a function does not result in an observable property (as would be the case when using `props`) but in an observable function, | ||
to make sure the compile time type matches the runtime type of the function. | ||
## Observing changes | ||
@@ -470,4 +483,4 @@ | ||
Observes the observable property `key` of `object`. This is useful if you want to observe properties created using the `observable` annotation or the `props` method, | ||
since for those properties their own `observe` method is not publicly available. | ||
Observes the observable property `key` of `object`. | ||
This is useful if you want to observe properties created using the `observable` annotation or the `props` method, since for those properties their own `observe` method is not publicly available. | ||
@@ -494,8 +507,6 @@ ```javascript | ||
So `func` will be evaluated only once, and as soon as its value has become stale, the `onInvalidate` callback is triggered. | ||
`watch` returns a tuple consisting of the initial return value of `func` and an unsubscriber to be able to abort the watch. | ||
The `onInvalidate` function will be called only once, after that, the watch has finished. | ||
`watch` returns a tuple consisting of the initial return value of `func` and an `unsubscriber` to be able to abort the watch. | ||
The `onInvalidate` function will be called only once, after that, the watch has finished. | ||
`watch` is useful in functions where you want to have a function that responds to change, | ||
but where the function is actually invoked as side effect or as part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive, | ||
for example in the `render` method of a React component. | ||
`watch` is useful in functions where you want to have a function that responds to change, but where the function is actually invoked as side effect or as part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive, e.g. in the `render` method of a React component. | ||
@@ -507,4 +518,4 @@ ### mobservable.batch | ||
Batch postpones the updates of computed properties until the (synchronous) `workerFunction` has completed. | ||
This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values, | ||
for example while refreshing a data from the database. In practice, you wil probably never need `.batch`, since observables usually update wickedly fast. | ||
This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values, e.g. while refreshing a data from the database. | ||
In practice, you wil probably never need `.batch`, since observables typically update wickedly fast. | ||
@@ -536,3 +547,3 @@ ```javascript | ||
Converts a (possibly) observable value into a non-observablue value. | ||
Converts a (possibly) observable value into a non-observablue value. | ||
For non-primitive values, this function will always return a shallow copy. | ||
@@ -542,7 +553,6 @@ | ||
The observer mixin can be used in [React](https://facebook.github.io/react/index.html) components. | ||
This mixin basically turns the `.render` function of the component into an observable function, and makes sure that the component itself becomes an observer of that function, | ||
so that the component is re-rendered each time an observable has changed. | ||
This mixin also prevents re-renderings when the *props* of the component have only shallowly changed | ||
(Similar to [React PureRender mixin](https://facebook.github.io/react/docs/pure-render-mixin.html), except that *state* changes are still always processed). | ||
The observer mixin can be used in [React](https://facebook.github.io/react/index.html) components. | ||
This mixin basically turns the `.render` function of the component into an observable function, and makes sure that the component itself becomes an observer of that function, that the component is re-rendered each time an observable has changed. | ||
This mixin also prevents re-renderings when the *props* of the component have only shallowly changed. | ||
(This is similar to [React PureRender mixin](https://facebook.github.io/react/docs/pure-render-mixin.html), except that *state* changes are still always processed). | ||
This allows for React apps that perform well in apps with large amount of complex data, while avoiding the need to manage a lot of subscriptions. | ||
@@ -552,3 +562,3 @@ | ||
For an extensive explanation, read [combing React with MOBservable](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/) | ||
For an extensive explanation, read [combing React with MOBservable](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/) | ||
@@ -558,4 +568,3 @@ ### mobservable.ObservingComponent | ||
If you want to create a React component based on ES6 where mixins are not supported, | ||
you can use the `ObservingComponent` function to wrap around your React `createClass` call (instead of using the mixin `ObserverMixin`): | ||
If you want to create a React component based on ES6 where mixins are not supported, you can use the `ObservingComponent` function to wrap around your React `createClass` call (instead of using the mixin `ObserverMixin`): | ||
@@ -576,7 +585,8 @@ ```javascript | ||
* `new mobservable.SimpleEventEmitter()`. Creates a new `SimpleEventEmitter` | ||
* `emit(...data : any[])`. Invokes all registered listeners with the given arguments | ||
* `on(listener:(...data : any[]) => void) : () => void`. Registers a new callback that will be invoked on each `emit`. Returns a method that can be used to unsubscribe the listener. | ||
* `once(listener:(...data : any[]) => void) : () => void`. Similar to `.on`, but automatically removes the listener after one invocation. | ||
* `new mobservable.SimpleEventEmitter()` Creates a new `SimpleEventEmitter`. | ||
* `emit(...data : any[])` Invokes all registered listeners with the given arguments. | ||
* `on(listener:(...data : any[]) => void) : () => void` Registers a new callback that will be invoked on each `emit`. Returns a method that can be used to unsubscribe the listener. | ||
* `once(listener:(...data : any[]) => void) : () => void` Similar to `.on`, but automatically removes the listener after one invocation. | ||
# Advanced Tips & Tricks | ||
@@ -586,10 +596,10 @@ | ||
All computed values are lazy and only evaluated upon first observation (or when their value is explicitly getted) | ||
All computed values are lazy and only evaluated upon first observation (or when their value is explicitly `get`-ted). | ||
## Use local variables in computations | ||
Each time an observable value is read, there is a small performance overhead to keep the dependency tree of computations up to date. | ||
Although this might not be noticable in practice, if you want to squeeze the last bit of performance out of the library; | ||
Each time an observable value is read, there is a small performance overhead to keep the dependency tree of computations up-to-date. | ||
Although this might not be noticeable in practice, if you want to squeeze the last bit of performance out of the library: | ||
use local variables as much as possible to reduce the amount of observable reads. | ||
This also holds for array entries and object properties created using `mobservable.props`. | ||
This also holds for array entries and object properties created using `mobservable.props`. | ||
@@ -620,3 +630,3 @@ ```javascript | ||
This is a useful pattern if you have an expensive computation that depends on a condition check that is fired often, but not changed often. | ||
For example when your computation contains a cheap treshold check, or when your UI renderig depends on the some selection of the user. | ||
For example when your computation contains a cheap threshold check, or when your UI rendering depends on the some selection of the user. | ||
For example: | ||
@@ -634,10 +644,8 @@ | ||
In the example above, every single time the person's `age` changes, `total` is computed by invoking some expensive computations. | ||
However, if the expression `page.age === 42` was put in a separate observable, | ||
computing the `total` itself could be avoided in many cases because a recomputation would only occur if the value of the complete expression changes. | ||
Yet, you might not want to create separate stand-alone observables for these expressions, | ||
because you don't have a nice place to put them or because it would make the readability of the code worse. | ||
In the example above, every single time the person's `age` changes, `total` is computed by invoking some expensive computations. | ||
However, if the expression `page.age === 42` was put in a separate observable, computing the `total` itself could be avoided in many cases because a re-computation would only occur if the value of the complete expression changes. | ||
Yet, you might not want to create separate stand-alone observables for these expressions, because you don't have a nice place to put them or because it would make the readability of the code worse. | ||
In such cases you can also create an inline observable. | ||
In the following example, the total is only recalculated if the age changes to, or from, 42. Which means that for most other ages, | ||
recomputing the expensive computations can be avoided. | ||
In the following example, the total is only recalculated if the age changes to, or from, 42. | ||
This means that for most other ages, recomputing the expensive computations can be avoided. | ||
@@ -655,3 +663,3 @@ ```javascript | ||
Note that the dangling `()` after the expression is meant to invoke the getter of the just created observable to obtain its value. | ||
Note that the dangling `()` (or "dog balls" according to Douglas Crockford) after the expression are meant to invoke the getter of the just created observable to obtain its value. | ||
For convenience the same statement can also be rewritten using the [expr](#mobservableexpr) function: | ||
@@ -700,7 +708,7 @@ | ||
The difference between `obj.amount = mobservable.value(3)` and `mobservable.props(obj, { value: 3 })` | ||
to create observable values inside an object might seem to be a matter of taste. | ||
The difference between `obj.amount = mobservable.value(3)` and `mobservable.props(obj, { value: 3 })` to create observable values inside an object might seem to be a matter of taste. | ||
Here is a small comparison list between the two approaches. | ||
**.value** | ||
* ES3 compliant | ||
@@ -712,3 +720,4 @@ * explicit getter/setter functions: `obj.amount(2)` | ||
**.props** | ||
* Requires ES5 | ||
* requires ES5 | ||
* object properties with implicit getter/setter: `obj.amount = 2` | ||
@@ -720,4 +729,6 @@ * more natural to write / read values, syntactically you won't notice they are observable | ||
Do *not* confuse `mobservable.reference([])` / `mobservable.primitive([])` with `mobservable([])` / `mobservable.array([])`, | ||
the first two create a observable reference to an array, but does not observe its contents. | ||
The later two observe the content of the array you passed into it, which is probably what you inteded. | ||
Do *not* confuse `mobservable.reference([])` / `mobservable.primitive([])` with `mobservable([])` / `mobservable.array([])`: | ||
* The first two create a observable reference to an array, but do not observe its contents. | ||
* The later two observe the content of the array you passed into it, which is probably what you intended. | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
158683
2004
692