From 7e1748309e9573167e630782c43b7153481bd3c0 Mon Sep 17 00:00:00 2001 From: Michael Best Date: Thu, 24 May 2012 14:37:26 -1000 Subject: [PATCH] #4 - re-evaluate computed only for change events, but queue for dirty events --- examples/knockout-2.0.0.js | 97 ----------------- examples/knockout-2.1.0.js | 86 +++++++++++++++ examples/nested-computed-noplugin.html | 2 +- examples/nested-computed-plugin.html | 2 +- knockout-deferred-updates.js | 139 +++++++++++++------------ 5 files changed, 163 insertions(+), 163 deletions(-) delete mode 100644 examples/knockout-2.0.0.js create mode 100644 examples/knockout-2.1.0.js diff --git a/examples/knockout-2.0.0.js b/examples/knockout-2.0.0.js deleted file mode 100644 index e5577eb..0000000 --- a/examples/knockout-2.0.0.js +++ /dev/null @@ -1,97 +0,0 @@ -// Knockout JavaScript library v2.0.0 -// (c) Steven Sanderson - http://knockoutjs.com/ -// License: MIT (http://www.opensource.org/licenses/mit-license.php) - -(function(window,undefined){ -function c(a){throw a;}var l=void 0,m=!0,o=null,p=!1,r=window.ko={};r.b=function(a,b){for(var d=a.split("."),e=window,f=0;f",b[0];);return 4r.a.k(e,a[b])&&e.push(a[b]);return e},ba:function(a,e){for(var a=a||[],b=[],f=0,d=a.length;fa.length?p:a.substring(0,e.length)===e},hb:function(a){for(var e=Array.prototype.slice.call(arguments,1),b="return ("+a+")",f=0;f",""]||!d.indexOf("",""]||(!d.indexOf("",""]||[0,"",""];a="ignored
"+ -d[1]+a+d[2]+"
";for("function"==typeof window.innerShiv?b.appendChild(window.innerShiv(a)):b.innerHTML=a;d[0]--;)b=b.lastChild;b=r.a.X(b.lastChild.childNodes)}return b};r.a.Z=function(a,b){r.a.U(a);if(b!==o&&b!==l)if("string"!=typeof b&&(b=b.toString()),"undefined"!=typeof jQuery)jQuery(a).html(b);else for(var d=r.a.ma(b),e=0;e"},Ra:function(a,b){var h=d[a];h===l&&c(Error("Couldn't find any memo with ID "+ -a+". Perhaps it's already been unmemoized."));try{return h.apply(o,b||[]),m}finally{delete d[a]}},Sa:function(a,f){var d=[];b(a,d);for(var g=0,i=d.length;gb;b++)a=a();return a})};r.toJSON=function(a){a=r.Pa(a);return r.a.qa(a)}})();r.b("ko.toJS",r.Pa);r.b("ko.toJSON",r.toJSON); -r.h={q:function(a){return"OPTION"==a.tagName?a.__ko__hasDomDataOptionValue__===m?r.a.e.get(a,r.c.options.la):a.getAttribute("value"):"SELECT"==a.tagName?0<=a.selectedIndex?r.h.q(a.options[a.selectedIndex]):l:a.value},S:function(a,b){if("OPTION"==a.tagName)switch(typeof b){case "string":r.a.e.set(a,r.c.options.la,l);"__ko__hasDomDataOptionValue__"in a&&delete a.__ko__hasDomDataOptionValue__;a.value=b;break;default:r.a.e.set(a,r.c.options.la,b),a.__ko__hasDomDataOptionValue__=m,a.value="number"===typeof b? -b:""}else if("SELECT"==a.tagName)for(var d=a.options.length-1;0<=d;d--){if(r.h.q(a.options[d])==b){a.selectedIndex=d;break}}else{if(b===o||b===l)b="";a.value=b}}};r.b("ko.selectExtensions",r.h);r.b("ko.selectExtensions.readValue",r.h.q);r.b("ko.selectExtensions.writeValue",r.h.S); -r.j=function(){function a(a,e){for(var d=o;a!=d;)d=a,a=a.replace(b,function(a,b){return e[b]});return a}var b=/\@ko_token_(\d+)\@/g,d=/^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i,e=["true","false"];return{D:[],Y:function(b){var e=r.a.z(b);if(3>e.length)return[];"{"===e.charAt(0)&&(e=e.substring(1,e.length-1));for(var b=[],d=o,i,j=0;j$/: -/^\s*ko\s+(.*\:.*)\s*$/,g=f?/^<\!--\s*\/ko\s*--\>$/:/^\s*\/ko\s*$/,i={ul:m,ol:m};r.f={C:{},childNodes:function(b){return a(b)?d(b):b.childNodes},ha:function(b){if(a(b))for(var b=r.f.childNodes(b),e=0,d=b.length;e"),p)}};r.c.uniqueName.Za=0; -r.c.checked={init:function(a,b,d){r.a.s(a,"click",function(){var e;if("checkbox"==a.type)e=a.checked;else if("radio"==a.type&&a.checked)e=a.value;else return;var f=b();"checkbox"==a.type&&r.a.d(f)instanceof Array?(e=r.a.k(r.a.d(f),a.value),a.checked&&0>e?f.push(a.value):!a.checked&&0<=e&&f.splice(e,1)):r.P(f)?f()!==e&&f(e):(f=d(),f._ko_property_writers&&f._ko_property_writers.checked&&f._ko_property_writers.checked(e))});"radio"==a.type&&!a.name&&r.c.uniqueName.init(a,function(){return m})},update:function(a, -b){var d=r.a.d(b());if("checkbox"==a.type)a.checked=d instanceof Array?0<=r.a.k(d,a.value):d;else if("radio"==a.type)a.checked=a.value==d}};r.c.attr={update:function(a,b){var d=r.a.d(b())||{},e;for(e in d)if("string"==typeof e){var f=r.a.d(d[e]);f===p||f===o||f===l?a.removeAttribute(e):a.setAttribute(e,f.toString())}}}; -r.c.hasfocus={init:function(a,b,d){function e(a){var e=b();a!=r.a.d(e)&&(r.P(e)?e(a):(e=d(),e._ko_property_writers&&e._ko_property_writers.hasfocus&&e._ko_property_writers.hasfocus(a)))}r.a.s(a,"focus",function(){e(m)});r.a.s(a,"focusin",function(){e(m)});r.a.s(a,"blur",function(){e(p)});r.a.s(a,"focusout",function(){e(p)})},update:function(a,b){var d=r.a.d(b());d?a.focus():a.blur();r.a.sa(a,d?"focusin":"focusout")}}; -r.c["with"]={o:function(a){return function(){var b=a();return{"if":b,data:b,templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c["with"].o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c["with"].o(b),d,e,f)}};r.j.D["with"]=p;r.f.C["with"]=m;r.c["if"]={o:function(a){return function(){return{"if":a(),templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c["if"].o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c["if"].o(b),d,e,f)}}; -r.j.D["if"]=p;r.f.C["if"]=m;r.c.ifnot={o:function(a){return function(){return{ifnot:a(),templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c.ifnot.o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c.ifnot.o(b),d,e,f)}};r.j.D.ifnot=p;r.f.C.ifnot=m; -r.c.foreach={o:function(a){return function(){var b=r.a.d(a());return!b||"number"==typeof b.length?{foreach:b,templateEngine:r.p.M}:{foreach:b.data,includeDestroyed:b.includeDestroyed,afterAdd:b.afterAdd,beforeRemove:b.beforeRemove,afterRender:b.afterRender,templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c.foreach.o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c.foreach.o(b),d,e,f)}};r.j.D.foreach=p;r.f.C.foreach=m;r.b("ko.allowedVirtualElementBindings",r.f.C); -r.t=function(){};r.t.prototype.renderTemplateSource=function(){c("Override renderTemplateSource")};r.t.prototype.createJavaScriptEvaluatorBlock=function(){c("Override createJavaScriptEvaluatorBlock")};r.t.prototype.makeTemplateSource=function(a){if("string"==typeof a){var b=document.getElementById(a);b||c(Error("Cannot find template with ID "+a));return new r.m.g(b)}if(1==a.nodeType||8==a.nodeType)return new r.m.I(a);c(Error("Unknown template type: "+a))}; -r.t.prototype.renderTemplate=function(a,b,d){return this.renderTemplateSource(this.makeTemplateSource(a),b,d)};r.t.prototype.isTemplateRewritten=function(a){return this.allowTemplateRewriting===p?m:this.W&&this.W[a]?m:this.makeTemplateSource(a).data("isRewritten")};r.t.prototype.rewriteTemplate=function(a,b){var d=this.makeTemplateSource(a),e=b(d.text());d.text(e);d.data("isRewritten",m);if("string"==typeof a)this.W=this.W||{},this.W[a]=m};r.b("ko.templateEngine",r.t); -r.$=function(){function a(a,b,d){for(var a=r.j.Y(a),g=r.j.D,i=0;i/g;return{gb:function(a,b){b.isTemplateRewritten(a)||b.rewriteTemplate(a,function(a){return r.$.ub(a,b)})},ub:function(e,f){return e.replace(b,function(b,e,d,j,k,n,t){return a(t,e,f)}).replace(d,function(b,e){return a(e,"<\!-- ko --\>",f)})},Ua:function(a){return r.r.ka(function(b,d){b.nextSibling&&r.xa(b.nextSibling,a,d)})}}}();r.b("ko.templateRewriting",r.$);r.b("ko.templateRewriting.applyMemoizedBindingsToNextSibling",r.$.Ua);r.m={};r.m.g=function(a){this.g=a}; -r.m.g.prototype.text=function(){if(0==arguments.length)return"script"==this.g.tagName.toLowerCase()?this.g.text:this.g.innerHTML;var a=arguments[0];"script"==this.g.tagName.toLowerCase()?this.g.text=a:r.a.Z(this.g,a)};r.m.g.prototype.data=function(a){if(1===arguments.length)return r.a.e.get(this.g,"templateSourceData_"+a);r.a.e.set(this.g,"templateSourceData_"+a,arguments[1])};r.m.I=function(a){this.g=a};r.m.I.prototype=new r.m.g; -r.m.I.prototype.text=function(){if(0==arguments.length)return r.a.e.get(this.g,"__ko_anon_template__");r.a.e.set(this.g,"__ko_anon_template__",arguments[0])};r.b("ko.templateSources",r.m);r.b("ko.templateSources.domElement",r.m.g);r.b("ko.templateSources.anonymousTemplate",r.m.I); -(function(){function a(a,b,d){for(var g=0;node=a[g];g++)node.parentNode===b&&(1===node.nodeType||8===node.nodeType)&&d(node)}function b(a,b,h,g,i){var i=i||{},j=i.templateEngine||d;r.$.gb(h,j);h=j.renderTemplate(h,g,i);("number"!=typeof h.length||0a&&c(Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later."));var h=d.data("precompiled");h||(h=d.text()||"",h=jQuery.template(o,"{{ko_with $item.koBindingContext}}"+h+"{{/ko_with}}"),d.data("precompiled",h)); -d=[e.$data];e=jQuery.extend({koBindingContext:e},f.templateOptions);e=jQuery.tmpl(h,d,e);e.appendTo(document.createElement("div"));jQuery.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+a+" })()) }}"};this.addTemplate=function(a,b){document.write(" + + diff --git a/knockout-deferred-updates.js b/knockout-deferred-updates.js index 55807a2..2b60932 100644 --- a/knockout-deferred-updates.js +++ b/knockout-deferred-updates.js @@ -1,6 +1,7 @@ // Deferred Updates plugin for Knockout http://knockoutjs.com/ // (c) Michael Best, Steven Sanderson // License: MIT (http://www.opensource.org/licenses/mit-license.php) +// Version 1.1.0 (function(ko, undefined) { @@ -21,13 +22,16 @@ ko.tasks = (function() { processEvaluators(originalLength); } + function currentStart() { + return taskStack.length ? taskStack[taskStack.length-1] : 0; + } + function processEvaluators(start) { - // New items might be added to evaluatorsArray during this loop - // So always check evaluatorsArray.length try { + // New items might be added to evaluatorsArray during this loop + // So always check evaluatorsArray.length for (var i = start || 0; i < evaluatorsArray.length; i++) { - if (!start) - indexProcessing = i; + indexProcessing = i; var evObj = evaluatorsArray[i], evaluator = evObj.evaluator; // Check/set a flag for the evaluator so we don't call it again if processEvaluators is called recursively if (!evObj.processed) { @@ -42,8 +46,9 @@ ko.tasks = (function() { } else { // Clear array and handler to indicate that we're finished evaluatorsArray = []; - indexProcessing = evaluatorHandler = undefined; + evaluatorHandler = undefined; } + indexProcessing = undefined; } } @@ -53,8 +58,8 @@ ko.tasks = (function() { } function isEvaluatorDuplicate(evaluator, extras) { - for (var i = indexProcessing || 0, j = evaluatorsArray.length; i < j; i++) - if (evaluatorsArray[i].evaluator == evaluator) { + for (var i = indexProcessing || currentStart(), j = evaluatorsArray.length; i < j; i++) + if (evaluatorsArray[i].evaluator == evaluator && !evaluatorsArray[i].processed) { if (extras) ko.utils.extend(evaluatorsArray[i], extras); return true; @@ -78,7 +83,7 @@ ko.tasks = (function() { return false; } evaluatorsArray.push(ko.utils.extend({evaluator: evaluator}, extras || {})); - if (!taskStack.length && indexProcessing === undefined && !evaluatorHandler) { + if (!taskStack.length && !evaluatorHandler) { evaluatorHandler = window[setImmediate](processEvaluatorsCallback); } return true; @@ -91,18 +96,14 @@ ko.tasks = (function() { } }; - ko.processDeferredBindingUpdatesForNode = function(node) { - for (var i = 0, j = evaluatorsArray.length; i < j; i++) { - if (evaluatorsArray[i].node == node) { - var evaluator = evaluatorsArray[i].evaluator; - evaluator(); - } - } - }; - + ko.processDeferredBindingUpdatesForNode = // deprecated (included for compatibility) ko.processAllDeferredBindingUpdates = function(node) { - for (var i = 0, j = evaluatorsArray.length; i < j; i++) { - if (evaluatorsArray[i].node) { + // New items might be added to evaluatorsArray during this loop + // So always check evaluatorsArray.length + for (var i = 0; i < evaluatorsArray.length; i++) { + var evObj = evaluatorsArray[i]; + if (evObj.node && !evObj.processed) { + evObj.processed = true; var evaluator = evaluatorsArray[i].evaluator; evaluator(); } @@ -179,18 +180,21 @@ ko.ignoreDependencies = function(callback, object, args) { * Replace ko.subscribable.fn.subscribe with one where change events are deferred */ subFnObj.oldSubscribe = subFnObj[subFnName]; // Save old subscribe function -subFnObj[subFnName] = function (callback, callbackTarget, event, deferUpdates) { - if (callback.toString().indexOf('throttleEvaluation') === -1) { +subFnObj[subFnName] = function (callback, callbackTarget, event, deferUpdates, isComputed) { + event = event || 'change'; + if (!isComputed) { var newCallback = function(valueToNotify) { - if (((newComputed.deferUpdates && deferUpdates !== false) || deferUpdates) && (!event || event == 'change')) - ko.tasks.processDelayed(callback, false, {object: callbackTarget, args: [valueToNotify]}); + if (((newComputed.deferUpdates && deferUpdates !== false) || deferUpdates) && event == 'change') + ko.tasks.processDelayed(callback, false, {object: callbackTarget, args: [valueToNotify, event]}); else - ko.ignoreDependencies(callback, callbackTarget, [valueToNotify]); + ko.ignoreDependencies(callback, callbackTarget, [valueToNotify, event]); }; - var subscription = this.oldSubscribe(newCallback, undefined, event); } else { - var subscription = this.oldSubscribe(callback, callbackTarget, event); + var newCallback = function(valueToNotify) { + callback(valueToNotify, event); + }; } + var subscription = this.oldSubscribe(newCallback, null, event); subscription.target = this; return subscription; } @@ -201,6 +205,7 @@ subFnObj[subFnName] = function (callback, callbackTarget, event, deferUpdates) { */ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) { var _latestValue, + _possiblyNeedsEvaluation = false, _needsEvaluation = true, _isBeingEvaluated = false, readFunction = evaluatorFunctionOrOptions; @@ -229,36 +234,48 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, subscription.dispose(); }); _subscriptionsToDependencies = []; - _needsEvaluation = false; + _possiblyNeedsEvaluation = _needsEvaluation = false; } var evaluationTimeoutInstance = null; - function evaluatePossiblyAsync() { - var shouldNotify = !_needsEvaluation; - _needsEvaluation = true; + function evaluatePossiblyAsync(value, event) { + var isDirtyEvent = (event == "dirty"); + var shouldNotify = isDirtyEvent && !_possiblyNeedsEvaluation && !_needsEvaluation; + if (isDirtyEvent) + _possiblyNeedsEvaluation = true; + else + _needsEvaluation = true; var throttleEvaluationTimeout = dependentObservable['throttleEvaluation']; if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { clearTimeout(evaluationTimeoutInstance); evaluationTimeoutInstance = ko.evaluateAsynchronously(evaluateImmediate, throttleEvaluationTimeout); } else if ((newComputed.deferUpdates && dependentObservable.deferUpdates !== false) || dependentObservable.deferUpdates) shouldNotify = ko.tasks.processDelayed(evaluateImmediate, true, {node: disposeWhenNodeIsRemoved}); - else + else if (_needsEvaluation) shouldNotify = evaluateImmediate(); if (shouldNotify && dependentObservable["notifySubscribers"]) { // notifySubscribers won't exist on first evaluation (but there won't be any subscribers anyway) dependentObservable["notifySubscribers"](_latestValue, "dirty"); - if (!_needsEvaluation && throttleEvaluationTimeout) // The notification might have triggered an evaluation + if (!_possiblyNeedsEvaluation && throttleEvaluationTimeout) // The notification might have triggered an evaluation clearTimeout(evaluationTimeoutInstance); } } + function markAsChanged() { + _needsEvaluation = true; + } + function addDependency(subscribable) { - var event = (subscribable[koProtoName] === newComputed) ? "dirty" : "change"; - _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync, null, event)); + var event = "change"; + if (subscribable[koProtoName] === newComputed) { + _subscriptionsToDependencies.push(subscribable.subscribe(markAsChanged, null, "change", false, true)); + event = "dirty"; + } + _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync, null, event, false, true)); } - function evaluateImmediate() { - if (_isBeingEvaluated || !_needsEvaluation) + function evaluateImmediate(force) { + if (_isBeingEvaluated || (!_needsEvaluation && !(force === true))) return false; // disposeWhen won't be set until after initial evaluation @@ -274,10 +291,12 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, var disposalCandidates = ko.utils.arrayMap(_subscriptionsToDependencies, function(item) {return item.target;}); depDet[depDetBeginName](function(subscribable) { - var inOld; - if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0) + var inOld, found = false; + while ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0) { disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used - else + found = true; + } + if (!found) addDependency(subscribable); // Brand new subscription - add it }); @@ -289,7 +308,7 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, _subscriptionsToDependencies.splice(i, 1)[0].dispose(); } - _needsEvaluation = false; + _possiblyNeedsEvaluation = _needsEvaluation = false; dependentObservable["notifySubscribers"](_latestValue, "beforeChange"); _latestValue = newValue; @@ -315,36 +334,28 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, function dependentObservable() { if (arguments.length > 0) { - set.apply(dependentObservable, arguments); - } else { - return get(); - } - } + if (typeof writeFunction === "function") { + // Writing a value + // Turn off deferred updates for this observable during the write so that the 'write' is registered + // immediately (assuming that the read function accesses any observables that are written to). + var saveDeferValue = dependentObservable.deferUpdates; + dependentObservable.deferUpdates = false; - function set() { - if (typeof writeFunction === "function") { - // Writing a value - // Turn off deferred updates for this observable during the write so that the 'write' is registered - // immediately (assuming that the read function accesses any observables that are written to). - var saveDeferValue = dependentObservable.deferUpdates; - dependentObservable.deferUpdates = false; + writeFunction.apply(evaluatorFunctionTarget, arguments); - writeFunction.apply(evaluatorFunctionTarget, arguments); - - dependentObservable.deferUpdates = saveDeferValue; + dependentObservable.deferUpdates = saveDeferValue; + } else { + throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters."); + } } else { - throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters."); + // Reading the value + if (_needsEvaluation || _possiblyNeedsEvaluation) + evaluateImmediate(true); + depDet[depDetRegisterName](dependentObservable); + return _latestValue; } } - function get() { - // Reading the value - if (_needsEvaluation) - evaluateImmediate(); - depDet[depDetRegisterName](dependentObservable); - return _latestValue; - } - // Need to set disposeWhenNodeIsRemoved here in case we get a notification during the initial evaluation var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;