diff --git a/README.md b/README.md index 18caa31..bc8d6e8 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,22 @@ -**Deferred Updates** plugin for [Knockout](http://knockoutjs.com/) +### **Deferred Updates** plugin for [Knockout](http://knockoutjs.com/) -This plugin/patch replaces `ko.computed` (and `ko.dependentObservable`) with a new version that supports **deferred updates**. Unlike the *throttle* feature, which schedules each update using individual `setTimeout` calls, *defer* schedules all updates to run together and eliminates duplicate updates. +This plugin/patch modifies parts of Knockout’s observable/subscription system to use **deferred updates**. -*Deferred updates* uses a new object called `ko.tasks` to handle the scheduling. `ko.tasks` has three options for scheduling (fastest to slowest): +* **Automatically eliminates duplicate updates.** Instead of updating a computed observable immediately each time one of its dependencies change, this plugin defers updates to computed observables so that mulitple dependency changes are combined into a single update. +* **It just works.** This plugin is compatible with most applications built with Knockout. Just include it in the page after *Knockout* to provide an immediate performance boost. +* **Better than throttle:** Like the *throttle* feature, deferred updates are generally run asynchronously using `setTimeout`. But whereas throttled updates are each scheduled individually with separate `setTimeout` calls, deferred updates run all together using a single `setTimeout`. +* **Control when updates occur.** By default, deferred updates occur in a `setTimeout` callback. But the user can make updates happen earlier: + * *Access a computed observable to update it.* A computed observable whose dependencies have changed is marked as *dirty* until it is updated. Accessing a *dirty* computed observable will cause it to update first. + * *Use `processImmediate` to wrap data changes.* Before it returns, `processImmediate` performs any deferred updates that were triggered by the data changes. + * *Use the `processDeferredBindingUpdates` functions to update bindings.* This plugin also defers UI updates since they use the same update system. If you have code that accesses the DOM directly and you’ve made data changes that will trigger a UI update, you’ll want to update the UI first. (See *Notes* below). + * *Include `setImmediate` for faster updates.* This plugin will use [setImmediate](https://github.com/NobleJS/setImmediate), if available, which enables updates to run without the minimum delay enforced by `setTimeout` (4 ms on modern browsers, 10-15 ms on older browsers). -1. If your code that updates observables is called using `ko.tasks.processImmediate`, `ko.tasks` will run all deferred updates immediately after your code completes. -2. If you include [setImmediate](https://github.com/NobleJS/setImmediate), `ko.tasks` will use it to schedule the updates. `setImmediate` enables the browser to run the updates immediately after all pending events (and UI updates (except in IE 8 and lower)) are processed. -3. As a last option, it will use `setTimeout` to schedule the updates. Since the updates are run using a single `setTimout`, the maximum delay will be the minimum `setTimout` interval (4 ms on modern browsers, 10-15 ms on older browsers). - -In addition to adding *deferred updates*, this plugin also includes these changes to `ko.computed`: - -1. `ko.computed` prevents recursive calls to itself. -2. `ko.computed`, when accessed, will always return the latest value. Previously, computed observables that use throttling would return a stale value if the scheduled update hadn’t occurred yet. With this change, when a computed observable with a pending update is accessed, the update will occur immediately and the scheduled update will be canceled. This change affects computed observables that use either *throttle* or *defer* and thus improves the *throttle* feature when *throttled* computed observables depend on other *throttled* ones. -3. The *throttle* extender will *either* delay evaluations or delay writes (but not both) based on whether the target observable is writable. - -Examples: +##### Examples * [Nested Computed with plugin](http://mbest.github.com/knockout-deferred-updates/examples/nested-computed-plugin.html) * [Nested Computed without plugin](http://mbest.github.com/knockout-deferred-updates/examples/nested-computed-noplugin.html) -Here are the new interfaces in this plugin: +##### New interfaces 1. `ko.tasks` * `processImmediate` takes three parameters: The first is the function you want it to run; next (optional) is the object the function should be called with (object will become `this` in the function); third (optional) is an array of values to pass to the function. By using `processImmediate` to call a function that updates observables, deferred updates to *dirtied* computed observables will be run as soon as your function completes. `processImmediate` will *not* run pending updates that were triggered before it was run. This allows nested calls to `processImmediate`. @@ -31,10 +28,14 @@ Here are the new interfaces in this plugin: 3. `ko.evaluateAsynchronously` is a replacement for `setTimeout` that will call the provided callback function within `ko.tasks.processImmediate`. 4. `ko.processDeferredBindingUpdatesForNode` and `ko.processAllDeferredBindingUpdates` provide a way to update the UI immediately. The first takes a *node* parameter and only processes updates for the specified node. The second processes all pending UI updates. You could use these functions if you have code that updates observables and then does direct DOM access, expecting it to be updated. Alternatively, you could wrap your observable updates in a call to `ko.tasks.processImmediate` (see above). -Notes: +##### Notes -* *Knockout* uses `ko.computed` internally to handle updates to bindings (so that updating an observable updates the UI). Because this plugin affects all computed observables, it defers binding updates too. This could be an advantage (fewer UI updates if bindings have multiple dependencies) or a disadvantage (slightly delayed updates). It also mean that this plugin will break code that assumes that the UI is updated immediately; that code will have to be modified to use either `processImmediate` to wrap the observable updates or one of the `DeferredBindingUpdates` functions before any direct DOM access. +1. In addition to adding *deferred updates*, this plugin also includes these changes to `ko.computed`: + 1. `ko.computed` prevents recursive calls to itself. + 2. `ko.computed`, when accessed, will always return the latest value. Previously, computed observables that use throttling would return a stale value if the scheduled update hadn’t occurred yet. With this change, when a computed observable with a pending update is accessed, the update will occur immediately and the scheduled update will be canceled. This change affects computed observables that use either *throttle* or *defer* and thus improves the *throttle* feature when *throttled* computed observables depend on other *throttled* ones. + 3. The *throttle* extender will *either* delay evaluations or delay writes (but not both) based on whether the target observable is writable. +2. *Knockout* uses `ko.computed` internally to handle updates to bindings (so that updating an observable updates the UI). Because this plugin affects all computed observables, it defers binding updates too. This could be an advantage (fewer UI updates if bindings have multiple dependencies) or a disadvantage (slightly delayed updates). It also mean that this plugin will break code that assumes that the UI is updated immediately; that code will have to be modified to use either `processImmediate` to wrap the observable updates or one of the `DeferredBindingUpdates` functions before any direct DOM access. -Michael Best -https://github.com/mbest/ +Michael Best
+https://github.com/mbest/
mbest@dasya.com diff --git a/knockout-deferred-updates.js b/knockout-deferred-updates.js index 8d8a486..95ab37b 100644 --- a/knockout-deferred-updates.js +++ b/knockout-deferred-updates.js @@ -71,12 +71,13 @@ ko.tasks = (function() { processDelayed: function(evaluator, distinct, extras) { if ((distinct || distinct === undefined) && isEvaluatorDuplicate(evaluator, extras)) { // Don't add evaluator if distinct is set (or missing) and evaluator is already in list - return; + return false; } evaluatorsArray.push(ko.utils.extend({evaluator: evaluator}, extras || {})); if (!taskStack.length && indexProcessing === undefined && !evaluatorHandler) { evaluatorHandler = window[setImmediate](processEvaluatorsCallback); } + return true; }, makeProcessedCallback: function(evaluator) { @@ -150,7 +151,7 @@ var oldComputed = ko.computed, disposeName = findPropertyName(computedProto, computedProto.dispose); // Find ko.utils.domNodeIsAttachedToDocument -var nodeInDocName = findNameMethodSignatureContaining(ko.utils, 'document)'); +var nodeInDocName = findNameMethodSignatureContaining(ko.utils, 'ocument)'); // Find the name of the ko.subscribable.fn.subscribe function var subFnObj = ko.subscribable.fn, @@ -169,7 +170,7 @@ ko.ignoreDependencies = function(callback, object, args) { } /* - * Replace ko.subscribable.fn.subscribe with one where change event are deferred + * 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) { @@ -180,10 +181,12 @@ subFnObj[subFnName] = function (callback, callbackTarget, event, deferUpdates) { else ko.ignoreDependencies(callback, callbackTarget, [valueToNotify]); }; - return this.oldSubscribe(newCallback, undefined, event); + var subscription = this.oldSubscribe(newCallback, undefined, event); } else { - return this.oldSubscribe(callback, callbackTarget, event); + var subscription = this.oldSubscribe(callback, callbackTarget, event); } + subscription.target = this; + return subscription; } @@ -225,17 +228,18 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, var evaluationTimeoutInstance = null; function evaluatePossiblyAsync() { + var shouldNotify = !_needsEvaluation; _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) - ko.tasks.processDelayed(evaluateImmediate, true, {node: disposeWhenNodeIsRemoved}); + shouldNotify = ko.tasks.processDelayed(evaluateImmediate, true, {node: disposeWhenNodeIsRemoved}); else - evaluateImmediate(); + shouldNotify = evaluateImmediate(); - if (dependentObservable["notifySubscribers"]) { + 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 clearTimeout(evaluationTimeoutInstance); @@ -249,28 +253,47 @@ var newComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, function evaluateImmediate() { if (_isBeingEvaluated || !_needsEvaluation) - return; + return false; // disposeWhen won't be set until after initial evaluation if (disposeWhen && disposeWhen()) { dependentObservable.dispose(); - return; + return false; } _isBeingEvaluated = true; try { - disposeAllSubscriptionsToDependencies(); - depDet[depDetBeginName](addDependency); + // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal). + // Then, during evaluation, we cross off any that are in fact still being used. + 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) + disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used + else + addDependency(subscribable); // Brand new subscription - add it + }); + var newValue = readFunction.call(evaluatorFunctionTarget); + + // For each subscription no longer being used, remove it from the active subscriptions list and dispose it + for (var i = disposalCandidates.length - 1; i >= 0; i--) { + if (disposalCandidates[i]) + _subscriptionsToDependencies.splice(i, 1)[0].dispose(); + } + + _needsEvaluation = false; + dependentObservable["notifySubscribers"](_latestValue, "beforeChange"); _latestValue = newValue; - _needsEvaluation = false; } finally { depDet.end(); } dependentObservable["notifySubscribers"](_latestValue); _isBeingEvaluated = false; + return true; } function evaluateInitial() {