diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js index bfcd0d02585bb..0210fa37bb9ad 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js @@ -45,7 +45,7 @@ define([ return false; } - steps.sort(this.sortItems).forEach(function (element) { + steps().sort(this.sortItems).forEach(function (element) { if (element.code == hashString || element.alias == hashString) { //eslint-disable-line eqeqeq element.navigate(element); } else { @@ -111,7 +111,7 @@ define([ getActiveItemIndex: function () { var activeIndex = 0; - steps.sort(this.sortItems).forEach(function (element, index) { + steps().sort(this.sortItems).forEach(function (element, index) { if (element.isVisible()) { activeIndex = index; } @@ -126,7 +126,7 @@ define([ */ isProcessed: function (code) { var activeItemIndex = this.getActiveItemIndex(), - sortedItems = steps.sort(this.sortItems), + sortedItems = steps().sort(this.sortItems), requestedItemIndex = -1; sortedItems.forEach(function (element, index) { @@ -143,7 +143,7 @@ define([ * @param {*} scrollToElementId */ navigateTo: function (code, scrollToElementId) { - var sortedItems = steps.sort(this.sortItems), + var sortedItems = steps().sort(this.sortItems), bodyElem = $.browser.safari || $.browser.chrome ? $('body') : $('html'); scrollToElementId = scrollToElementId || null; @@ -179,7 +179,7 @@ define([ var activeIndex = 0, code; - steps.sort(this.sortItems).forEach(function (element, index) { + steps().sort(this.sortItems).forEach(function (element, index) { if (element.isVisible()) { element.isVisible(false); activeIndex = index; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/simple-checked.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/simple-checked.js index faddfb609924e..188d86c71d07a 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/simple-checked.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/simple-checked.js @@ -68,7 +68,7 @@ define([ } }; - ko.expressionRewriting.twoWayBindings.simpleChecked = true; + ko.expressionRewriting._twoWayBindings.simpleChecked = true; renderer.addAttribute('simpleChecked'); renderer.addAttribute('simple-checked', { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/staticChecked.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/staticChecked.js index 44a3d1f6a06b3..82e3dfecf99a0 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/staticChecked.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/staticChecked.js @@ -100,7 +100,7 @@ define([ } }; - ko.expressionRewriting.twoWayBindings.staticChecked = true; + ko.expressionRewriting._twoWayBindings.staticChecked = true; renderer.addAttribute('staticChecked'); }); diff --git a/lib/web/knockoutjs/knockout.js b/lib/web/knockoutjs/knockout.js index 47af09ff36f2b..cd11f4d0c0d14 100644 --- a/lib/web/knockoutjs/knockout.js +++ b/lib/web/knockoutjs/knockout.js @@ -1,6 +1,6 @@ /*! - * Knockout JavaScript library v3.3.0 - * (c) Steven Sanderson - http://knockoutjs.com/ + * Knockout JavaScript library v3.4.2 + * (c) The Knockout.js team - http://knockoutjs.com/ * License: MIT (http://www.opensource.org/licenses/mit-license.php) */ @@ -19,7 +19,7 @@ var DEBUG=true; if (typeof define === 'function' && define['amd']) { // [1] AMD anonymous module define(['exports', 'require'], factory); - } else if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { + } else if (typeof exports === 'object' && typeof module === 'object') { // [2] CommonJS/Node.js factory(module['exports'] || exports); // module.exports is for Node.js } else { @@ -45,9 +45,16 @@ ko.exportSymbol = function(koPath, object) { ko.exportProperty = function(owner, publicName, object) { owner[publicName] = object; }; -ko.version = "3.3.0"; +ko.version = "3.4.2"; ko.exportSymbol('version', ko.version); +// For any options that may affect various areas of Knockout and aren't directly associated with data binding. +ko.options = { + 'deferUpdates': false, + 'useOnlyNativeEvents': false +}; + +//ko.exportSymbol('options', ko.options); // 'options' isn't minified ko.utils = (function () { function objectForEach(obj, action) { for (var prop in obj) { @@ -74,6 +81,7 @@ ko.utils = (function () { } var canSetPrototype = ({ __proto__: [] } instanceof Array); + var canUseSymbols = !DEBUG && typeof Symbol === 'function'; // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup) var knownEvents = {}, knownEventTypesByEventName = {}; @@ -304,8 +312,11 @@ ko.utils = (function () { // Rules: // [A] Any leading nodes that have been removed should be ignored // These most likely correspond to memoization nodes that were already removed during binding - // See https://github.com/SteveSanderson/knockout/pull/440 - // [B] We want to output a continuous series of nodes. So, ignore any nodes that have already been removed, + // See https://github.com/knockout/knockout/pull/440 + // [B] Any trailing nodes that have been remove should be ignored + // This prevents the code here from adding unrelated nodes to the array while processing rule [C] + // See https://github.com/knockout/knockout/pull/1903 + // [C] We want to output a continuous series of nodes. So, ignore any nodes that have already been removed, // and include any nodes that have been inserted among the previous collection if (continuousNodeArray.length) { @@ -317,6 +328,10 @@ ko.utils = (function () { continuousNodeArray.splice(0, 1); // Rule [B] + while (continuousNodeArray.length > 1 && continuousNodeArray[continuousNodeArray.length - 1].parentNode !== parentNode) + continuousNodeArray.length--; + + // Rule [C] if (continuousNodeArray.length > 1) { var current = continuousNodeArray[0], last = continuousNodeArray[continuousNodeArray.length - 1]; // Replace with the actual new continuous node set @@ -324,8 +339,6 @@ ko.utils = (function () { while (current !== last) { continuousNodeArray.push(current); current = current.nextSibling; - if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario) - return; } continuousNodeArray.push(last); } @@ -385,14 +398,38 @@ ko.utils = (function () { return element && element.tagName && element.tagName.toLowerCase(); }, + catchFunctionErrors: function (delegate) { + return ko['onError'] ? function () { + try { + return delegate.apply(this, arguments); + } catch (e) { + ko['onError'] && ko['onError'](e); + throw e; + } + } : delegate; + }, + + setTimeout: function (handler, timeout) { + return setTimeout(ko.utils.catchFunctionErrors(handler), timeout); + }, + + deferError: function (error) { + setTimeout(function () { + ko['onError'] && ko['onError'](error); + throw error; + }, 0); + }, + registerEventHandler: function (element, eventType, handler) { + var wrappedHandler = ko.utils.catchFunctionErrors(handler); + var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType]; - if (!mustUseAttachEvent && jQueryInstance) { - jQueryInstance(element)['bind'](eventType, handler); + if (!ko.options['useOnlyNativeEvents'] && !mustUseAttachEvent && jQueryInstance) { + jQueryInstance(element)['bind'](eventType, wrappedHandler); } else if (!mustUseAttachEvent && typeof element.addEventListener == "function") - element.addEventListener(eventType, handler, false); + element.addEventListener(eventType, wrappedHandler, false); else if (typeof element.attachEvent != "undefined") { - var attachEventHandler = function (event) { handler.call(element, event); }, + var attachEventHandler = function (event) { wrappedHandler.call(element, event); }, attachEventName = "on" + eventType; element.attachEvent(attachEventName, attachEventHandler); @@ -415,7 +452,7 @@ ko.utils = (function () { // In both cases, we'll use the click method instead. var useClickWorkaround = isClickOnCheckableElement(element, eventType); - if (jQueryInstance && !useClickWorkaround) { + if (!ko.options['useOnlyNativeEvents'] && jQueryInstance && !useClickWorkaround) { jQueryInstance(element)['trigger'](eventType); } else if (typeof document.createEvent == "function") { if (typeof element.dispatchEvent == "function") { @@ -515,6 +552,10 @@ ko.utils = (function () { return result; }, + createSymbolOrString: function(identifier) { + return canUseSymbols ? Symbol(identifier) : identifier; + }, + isIe6 : isIe6, isIe7 : isIe7, ieVersion : ieVersion, @@ -793,7 +834,29 @@ ko.exportSymbol('utils.domNodeDisposal', ko.utils.domNodeDisposal); ko.exportSymbol('utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback); ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback); (function () { - var leadingCommentRegex = /^(\s*)/; + var none = [0, "", ""], + table = [1, "", "
"], + tbody = [2, "", "
"], + tr = [3, "", "
"], + select = [1, ""], + lookup = { + 'thead': table, + 'tbody': table, + 'tfoot': table, + 'tr': tbody, + 'td': tr, + 'th': tr, + 'option': select, + 'optgroup': select + }, + + // This is needed for old IE if you're *not* using either jQuery or innerShiv. Doesn't affect other cases. + mayRequireCreateElementHack = ko.utils.ieVersion <= 8; + + function getWrap(tags) { + var m = tags.match(/^<([a-z]+)[ >]/); + return (m && lookup[m[1]]) || none; + } function simpleHtmlParse(html, documentContext) { documentContext || (documentContext = document); @@ -808,25 +871,34 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present. // Trim whitespace, otherwise indexOf won't work as expected - var tags = ko.utils.stringTrim(html).toLowerCase(), div = documentContext.createElement("div"); - - // Finds the first match from the left column, and returns the corresponding "wrap" data from the right column - var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "", "
"] || - !tags.indexOf("", ""] || - (!tags.indexOf("", ""] || - /* anything else */ [0, "", ""]; + var tags = ko.utils.stringTrim(html).toLowerCase(), div = documentContext.createElement("div"), + wrap = getWrap(tags), + depth = wrap[0]; // Go to html and back, then peel off extra wrappers // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness. var markup = "ignored
" + wrap[1] + html + wrap[2] + "
"; if (typeof windowContext['innerShiv'] == "function") { + // Note that innerShiv is deprecated in favour of html5shiv. We should consider adding + // support for html5shiv (except if no explicit support is needed, e.g., if html5shiv + // somehow shims the native APIs so it just works anyway) div.appendChild(windowContext['innerShiv'](markup)); } else { + if (mayRequireCreateElementHack) { + // The document.createElement('my-element') trick to enable custom elements in IE6-8 + // only works if we assign innerHTML on an element associated with that document. + documentContext.appendChild(div); + } + div.innerHTML = markup; + + if (mayRequireCreateElementHack) { + div.parentNode.removeChild(div); + } } // Move to the right depth - while (wrap[0]--) + while (depth--) div = div.lastChild; return ko.utils.makeArray(div.lastChild.childNodes); @@ -858,8 +930,9 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD } ko.utils.parseHtmlFragment = function(html, documentContext) { - return jQueryInstance ? jQueryHtmlParse(html, documentContext) // As below, benefit from jQuery's optimisations where possible - : simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases. + return jQueryInstance ? + jQueryHtmlParse(html, documentContext) : // As below, benefit from jQuery's optimisations where possible + simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases. }; ko.utils.setHtml = function(node, html) { @@ -959,6 +1032,114 @@ ko.exportSymbol('memoization.memoize', ko.memoization.memoize); ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize); ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText); ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants); +ko.tasks = (function () { + var scheduler, + taskQueue = [], + taskQueueLength = 0, + nextHandle = 1, + nextIndexToProcess = 0; + + if (window['MutationObserver']) { + // Chrome 27+, Firefox 14+, IE 11+, Opera 15+, Safari 6.1+ + // From https://github.com/petkaantonov/bluebird * Copyright (c) 2014 Petka Antonov * License: MIT + scheduler = (function (callback) { + var div = document.createElement("div"); + new MutationObserver(callback).observe(div, {attributes: true}); + return function () { div.classList.toggle("foo"); }; + })(scheduledProcess); + } else if (document && "onreadystatechange" in document.createElement("script")) { + // IE 6-10 + // From https://github.com/YuzuJS/setImmediate * Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola * License: MIT + scheduler = function (callback) { + var script = document.createElement("script"); + script.onreadystatechange = function () { + script.onreadystatechange = null; + document.documentElement.removeChild(script); + script = null; + callback(); + }; + document.documentElement.appendChild(script); + }; + } else { + scheduler = function (callback) { + setTimeout(callback, 0); + }; + } + + function processTasks() { + if (taskQueueLength) { + // Each mark represents the end of a logical group of tasks and the number of these groups is + // limited to prevent unchecked recursion. + var mark = taskQueueLength, countMarks = 0; + + // nextIndexToProcess keeps track of where we are in the queue; processTasks can be called recursively without issue + for (var task; nextIndexToProcess < taskQueueLength; ) { + if (task = taskQueue[nextIndexToProcess++]) { + if (nextIndexToProcess > mark) { + if (++countMarks >= 5000) { + nextIndexToProcess = taskQueueLength; // skip all tasks remaining in the queue since any of them could be causing the recursion + ko.utils.deferError(Error("'Too much recursion' after processing " + countMarks + " task groups.")); + break; + } + mark = taskQueueLength; + } + try { + task(); + } catch (ex) { + ko.utils.deferError(ex); + } + } + } + } + } + + function scheduledProcess() { + processTasks(); + + // Reset the queue + nextIndexToProcess = taskQueueLength = taskQueue.length = 0; + } + + function scheduleTaskProcessing() { + ko.tasks['scheduler'](scheduledProcess); + } + + var tasks = { + 'scheduler': scheduler, // Allow overriding the scheduler + + schedule: function (func) { + if (!taskQueueLength) { + scheduleTaskProcessing(); + } + + taskQueue[taskQueueLength++] = func; + return nextHandle++; + }, + + cancel: function (handle) { + var index = handle - (nextHandle - taskQueueLength); + if (index >= nextIndexToProcess && index < taskQueueLength) { + taskQueue[index] = null; + } + }, + + // For testing only: reset the queue and return the previous queue length + 'resetForTesting': function () { + var length = taskQueueLength - nextIndexToProcess; + nextIndexToProcess = taskQueueLength = taskQueue.length = 0; + return length; + }, + + runEarly: processTasks + }; + + return tasks; +})(); + +ko.exportSymbol('tasks', ko.tasks); +ko.exportSymbol('tasks.schedule', ko.tasks.schedule); +//ko.exportSymbol('tasks.cancel', ko.tasks.cancel); "cancel" isn't minified +ko.exportSymbol('tasks.runEarly', ko.tasks.runEarly); ko.extenders = { 'throttle': function(target, timeout) { // Throttling means two things: @@ -974,7 +1155,7 @@ ko.extenders = { 'read': target, 'write': function(value) { clearTimeout(writeTimeoutInstance); - writeTimeoutInstance = setTimeout(function() { + writeTimeoutInstance = ko.utils.setTimeout(function() { target(value); }, timeout); } @@ -991,12 +1172,42 @@ ko.extenders = { method = options['method']; } + // rateLimit supersedes deferred updates + target._deferUpdates = false; + limitFunction = method == 'notifyWhenChangesStop' ? debounce : throttle; target.limit(function(callback) { return limitFunction(callback, timeout); }); }, + 'deferred': function(target, options) { + if (options !== true) { + throw new Error('The \'deferred\' extender only accepts the value \'true\', because it is not supported to turn deferral off once enabled.') + } + + if (!target._deferUpdates) { + target._deferUpdates = true; + target.limit(function (callback) { + var handle, + ignoreUpdates = false; + return function () { + if (!ignoreUpdates) { + ko.tasks.cancel(handle); + handle = ko.tasks.schedule(callback); + + try { + ignoreUpdates = true; + target['notifySubscribers'](undefined, 'dirty'); + } finally { + ignoreUpdates = false; + } + } + }; + }); + } + }, + 'notify': function(target, notifyWhen) { target["equalityComparer"] = notifyWhen == "always" ? null : // null equalityComparer means to always notify @@ -1014,7 +1225,7 @@ function throttle(callback, timeout) { var timeoutInstance; return function () { if (!timeoutInstance) { - timeoutInstance = setTimeout(function() { + timeoutInstance = ko.utils.setTimeout(function () { timeoutInstance = undefined; callback(); }, timeout); @@ -1026,7 +1237,7 @@ function debounce(callback, timeout) { var timeoutInstance; return function () { clearTimeout(timeoutInstance); - timeoutInstance = setTimeout(callback, timeout); + timeoutInstance = ko.utils.setTimeout(callback, timeout); }; } @@ -1058,14 +1269,29 @@ ko.subscription.prototype.dispose = function () { }; ko.subscribable = function () { - ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']); - this._subscriptions = {}; - this._versionNumber = 1; + ko.utils.setPrototypeOfOrExtend(this, ko_subscribable_fn); + ko_subscribable_fn.init(this); } var defaultEvent = "change"; +// Moved out of "limit" to avoid the extra closure +function limitNotifySubscribers(value, event) { + if (!event || event === defaultEvent) { + this._limitChange(value); + } else if (event === 'beforeChange') { + this._limitBeforeChange(value); + } else { + this._origNotifySubscribers(value, event); + } +} + var ko_subscribable_fn = { + init: function(instance) { + instance._subscriptions = { "change": [] }; + instance._versionNumber = 1; + }, + subscribe: function (callback, callbackTarget, event) { var self = this; @@ -1094,9 +1320,10 @@ var ko_subscribable_fn = { this.updateVersion(); } if (this.hasSubscriptionsForEvent(event)) { + var subs = event === defaultEvent && this._changeSubscriptions || this._subscriptions[event].slice(0); try { ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined) - for (var a = this._subscriptions[event].slice(0), i = 0, subscription; subscription = a[i]; ++i) { + for (var i = 0, subscription; subscription = subs[i]; ++i) { // In case a subscription was disposed during the arrayForEach cycle, check // for isDisposed on each subscription before invoking its callback if (!subscription.isDisposed) @@ -1122,44 +1349,47 @@ var ko_subscribable_fn = { limit: function(limitFunction) { var self = this, selfIsObservable = ko.isObservable(self), - isPending, previousValue, pendingValue, beforeChange = 'beforeChange'; + ignoreBeforeChange, notifyNextChange, previousValue, pendingValue, beforeChange = 'beforeChange'; if (!self._origNotifySubscribers) { self._origNotifySubscribers = self["notifySubscribers"]; - self["notifySubscribers"] = function(value, event) { - if (!event || event === defaultEvent) { - self._rateLimitedChange(value); - } else if (event === beforeChange) { - self._rateLimitedBeforeChange(value); - } else { - self._origNotifySubscribers(value, event); - } - }; + self["notifySubscribers"] = limitNotifySubscribers; } var finish = limitFunction(function() { + self._notificationIsPending = false; + // If an observable provided a reference to itself, access it to get the latest value. // This allows computed observables to delay calculating their value until needed. if (selfIsObservable && pendingValue === self) { - pendingValue = self(); + pendingValue = self._evalIfChanged ? self._evalIfChanged() : self(); } - isPending = false; - if (self.isDifferent(previousValue, pendingValue)) { + var shouldNotify = notifyNextChange || self.isDifferent(previousValue, pendingValue); + + notifyNextChange = ignoreBeforeChange = false; + + if (shouldNotify) { self._origNotifySubscribers(previousValue = pendingValue); } }); - self._rateLimitedChange = function(value) { - isPending = true; + self._limitChange = function(value) { + self._changeSubscriptions = self._subscriptions[defaultEvent].slice(0); + self._notificationIsPending = ignoreBeforeChange = true; pendingValue = value; finish(); }; - self._rateLimitedBeforeChange = function(value) { - if (!isPending) { + self._limitBeforeChange = function(value) { + if (!ignoreBeforeChange) { previousValue = value; self._origNotifySubscribers(value, beforeChange); } }; + self._notifyNextChangeIfValueIsDifferent = function() { + if (self.isDifferent(previousValue, self.peek(true /*evaluate*/))) { + notifyNextChange = true; + } + }; }, hasSubscriptionsForEvent: function(event) { @@ -1172,7 +1402,8 @@ var ko_subscribable_fn = { } else { var total = 0; ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) { - total += subscriptions.length; + if (eventName !== 'dirty') + total += subscriptions.length; }); return total; } @@ -1239,7 +1470,7 @@ ko.computedContext = ko.dependencyDetection = (function () { if (currentFrame) { if (!ko.isSubscribable(subscribable)) throw new Error("Only subscribable things can act as dependencies"); - currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId())); + currentFrame.callback.call(currentFrame.callbackTarget, subscribable, subscribable._id || (subscribable._id = getId())); } }, @@ -1267,21 +1498,19 @@ ko.computedContext = ko.dependencyDetection = (function () { ko.exportSymbol('computedContext', ko.computedContext); ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount); ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial); -ko.exportSymbol('computedContext.isSleeping', ko.computedContext.isSleeping); ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore); -ko.observable = function (initialValue) { - var _latestValue = initialValue; +var observableLatestValue = ko.utils.createSymbolOrString('_latestValue'); +ko.observable = function (initialValue) { function observable() { if (arguments.length > 0) { // Write // Ignore writes if the value hasn't changed - if (observable.isDifferent(_latestValue, arguments[0])) { + if (observable.isDifferent(observable[observableLatestValue], arguments[0])) { observable.valueWillMutate(); - _latestValue = arguments[0]; - if (DEBUG) observable._latestValue = _latestValue; + observable[observableLatestValue] = arguments[0]; observable.valueHasMutated(); } return this; // Permits chained assignments @@ -1289,37 +1518,46 @@ ko.observable = function (initialValue) { else { // Read ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation - return _latestValue; + return observable[observableLatestValue]; } } - ko.subscribable.call(observable); - ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']); - if (DEBUG) observable._latestValue = _latestValue; - observable.peek = function() { return _latestValue }; - observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); } - observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); } + observable[observableLatestValue] = initialValue; + + // Inherit from 'subscribable' + if (!ko.utils.canSetPrototype) { + // 'subscribable' won't be on the prototype chain unless we put it there directly + ko.utils.extend(observable, ko.subscribable['fn']); + } + ko.subscribable['fn'].init(observable); - ko.exportProperty(observable, 'peek', observable.peek); - ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated); - ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate); + // Inherit from 'observable' + ko.utils.setPrototypeOfOrExtend(observable, observableFn); + + if (ko.options['deferUpdates']) { + ko.extenders['deferred'](observable, true); + } return observable; } -ko.observable['fn'] = { - "equalityComparer": valuesArePrimitiveAndEqual +// Define prototype for observables +var observableFn = { + 'equalityComparer': valuesArePrimitiveAndEqual, + peek: function() { return this[observableLatestValue]; }, + valueHasMutated: function () { this['notifySubscribers'](this[observableLatestValue]); }, + valueWillMutate: function () { this['notifySubscribers'](this[observableLatestValue], 'beforeChange'); } }; -var protoProperty = ko.observable.protoProperty = "__ko_proto__"; -ko.observable['fn'][protoProperty] = ko.observable; - // Note that for browsers that don't support proto assignment, the // inheritance chain is created manually in the ko.observable constructor if (ko.utils.canSetPrototype) { - ko.utils.setPrototypeOf(ko.observable['fn'], ko.subscribable['fn']); + ko.utils.setPrototypeOf(observableFn, ko.subscribable['fn']); } +var protoProperty = ko.observable.protoProperty = '__ko_proto__'; +observableFn[protoProperty] = ko.observable; + ko.hasPrototype = function(instance, prototype) { if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false; if (instance[protoProperty] === prototype) return true; @@ -1331,20 +1569,23 @@ ko.isObservable = function (instance) { } ko.isWriteableObservable = function (instance) { // Observable - if ((typeof instance == "function") && instance[protoProperty] === ko.observable) + if ((typeof instance == 'function') && instance[protoProperty] === ko.observable) return true; // Writeable dependent observable - if ((typeof instance == "function") && (instance[protoProperty] === ko.dependentObservable) && (instance.hasWriteFunction)) + if ((typeof instance == 'function') && (instance[protoProperty] === ko.dependentObservable) && (instance.hasWriteFunction)) return true; // Anything else return false; } - ko.exportSymbol('observable', ko.observable); ko.exportSymbol('isObservable', ko.isObservable); ko.exportSymbol('isWriteableObservable', ko.isWriteableObservable); ko.exportSymbol('isWritableObservable', ko.isWriteableObservable); +ko.exportSymbol('observable.fn', observableFn); +ko.exportProperty(observableFn, 'peek', observableFn.peek); +ko.exportProperty(observableFn, 'valueHasMutated', observableFn.valueHasMutated); +ko.exportProperty(observableFn, 'valueWillMutate', observableFn.valueWillMutate); ko.observableArray = function (initialValues) { initialValues = initialValues || []; @@ -1436,6 +1677,12 @@ ko.observableArray['fn'] = { } }; +// Note that for browsers that don't support proto assignment, the +// inheritance chain is created manually in the ko.observableArray constructor +if (ko.utils.canSetPrototype) { + ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']); +} + // Populate ko.observableArray.fn with read/write functions from native arrays // Important: Do not add any additional functions here that may reasonably be used to *read* data from the array // because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale @@ -1448,7 +1695,8 @@ ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "uns this.cacheDiffForKnownOperation(underlyingArray, methodName, arguments); var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments); this.valueHasMutated(); - return methodCallResult; + // The native sort and reverse methods return a reference to the array, but it makes more sense to return the observable array instead. + return methodCallResult === underlyingArray ? this : methodCallResult; }; }); @@ -1460,15 +1708,16 @@ ko.utils.arrayForEach(["slice"], function (methodName) { }; }); -// Note that for browsers that don't support proto assignment, the -// inheritance chain is created manually in the ko.observableArray constructor -if (ko.utils.canSetPrototype) { - ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']); -} - ko.exportSymbol('observableArray', ko.observableArray); var arrayChangeEventName = 'arrayChange'; -ko.extenders['trackArrayChanges'] = function(target) { +ko.extenders['trackArrayChanges'] = function(target, options) { + // Use the provided options--each call to trackArrayChanges overwrites the previously set options + target.compareArrayOptions = {}; + if (options && typeof options == "object") { + ko.utils.extend(target.compareArrayOptions, options); + } + target.compareArrayOptions['sparse'] = true; + // Only modify the target observable once if (target.cacheDiffForKnownOperation) { return; @@ -1477,6 +1726,7 @@ ko.extenders['trackArrayChanges'] = function(target) { cachedDiff = null, arrayChangeSubscription, pendingNotifications = 0, + underlyingNotifySubscribersFunction, underlyingBeforeSubscriptionAddFunction = target.beforeSubscriptionAdd, underlyingAfterSubscriptionRemoveFunction = target.afterSubscriptionRemove; @@ -1493,6 +1743,10 @@ ko.extenders['trackArrayChanges'] = function(target) { if (underlyingAfterSubscriptionRemoveFunction) underlyingAfterSubscriptionRemoveFunction.call(target, event); if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) { + if (underlyingNotifySubscribersFunction) { + target['notifySubscribers'] = underlyingNotifySubscribersFunction; + underlyingNotifySubscribersFunction = undefined; + } arrayChangeSubscription.dispose(); trackingChanges = false; } @@ -1507,7 +1761,7 @@ ko.extenders['trackArrayChanges'] = function(target) { trackingChanges = true; // Intercept "notifySubscribers" to track how many times it was called. - var underlyingNotifySubscribersFunction = target['notifySubscribers']; + underlyingNotifySubscribersFunction = target['notifySubscribers']; target['notifySubscribers'] = function(valueToNotify, event) { if (!event || event === defaultEvent) { ++pendingNotifications; @@ -1545,7 +1799,7 @@ ko.extenders['trackArrayChanges'] = function(target) { // plugin, which without this check would not be compatible with arrayChange notifications. Normally, // notifications are issued immediately so we wouldn't be queueing up more than one. if (!cachedDiff || pendingNotifications > 1) { - cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true }); + cachedDiff = ko.utils.compareArrays(previousContents, currentContents, target.compareArrayOptions); } return cachedDiff; @@ -1605,81 +1859,230 @@ ko.extenders['trackArrayChanges'] = function(target) { cachedDiff = diff; }; }; +var computedState = ko.utils.createSymbolOrString('_state'); + ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) { - var _latestValue, - _needsEvaluation = true, - _isBeingEvaluated = false, - _suppressDisposalUntilDisposeWhenReturnsFalse = false, - _isDisposed = false, - readFunction = evaluatorFunctionOrOptions, - pure = false, - isSleeping = false; - - if (readFunction && typeof readFunction == "object") { + if (typeof evaluatorFunctionOrOptions === "object") { // Single-parameter syntax - everything is on this "options" param - options = readFunction; - readFunction = options["read"]; + options = evaluatorFunctionOrOptions; } else { // Multi-parameter syntax - construct the options according to the params passed options = options || {}; - if (!readFunction) - readFunction = options["read"]; + if (evaluatorFunctionOrOptions) { + options["read"] = evaluatorFunctionOrOptions; + } } - if (typeof readFunction != "function") - throw new Error("Pass a function that returns the value of the ko.computed"); + if (typeof options["read"] != "function") + throw Error("Pass a function that returns the value of the ko.computed"); + + var writeFunction = options["write"]; + var state = { + latestValue: undefined, + isStale: true, + isDirty: true, + isBeingEvaluated: false, + suppressDisposalUntilDisposeWhenReturnsFalse: false, + isDisposed: false, + pure: false, + isSleeping: false, + readFunction: options["read"], + evaluatorFunctionTarget: evaluatorFunctionTarget || options["owner"], + disposeWhenNodeIsRemoved: options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null, + disposeWhen: options["disposeWhen"] || options.disposeWhen, + domNodeDisposalCallback: null, + dependencyTracking: {}, + dependenciesCount: 0, + evaluationTimeoutInstance: null + }; - function addDependencyTracking(id, target, trackingObj) { - if (pure && target === dependentObservable) { - throw Error("A 'pure' computed must not be called recursively"); + function computedObservable() { + if (arguments.length > 0) { + if (typeof writeFunction === "function") { + // Writing a value + writeFunction.apply(state.evaluatorFunctionTarget, arguments); + } 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."); + } + return this; // Permits chained assignments + } else { + // Reading the value + ko.dependencyDetection.registerDependency(computedObservable); + if (state.isDirty || (state.isSleeping && computedObservable.haveDependenciesChanged())) { + computedObservable.evaluateImmediate(); + } + return state.latestValue; } + } - dependencyTracking[id] = trackingObj; - trackingObj._order = _dependenciesCount++; - trackingObj._version = target.getVersion(); + computedObservable[computedState] = state; + computedObservable.hasWriteFunction = typeof writeFunction === "function"; + + // Inherit from 'subscribable' + if (!ko.utils.canSetPrototype) { + // 'subscribable' won't be on the prototype chain unless we put it there directly + ko.utils.extend(computedObservable, ko.subscribable['fn']); + } + ko.subscribable['fn'].init(computedObservable); + + // Inherit from 'computed' + ko.utils.setPrototypeOfOrExtend(computedObservable, computedFn); + + if (options['pure']) { + state.pure = true; + state.isSleeping = true; // Starts off sleeping; will awake on the first subscription + ko.utils.extend(computedObservable, pureComputedOverrides); + } else if (options['deferEvaluation']) { + ko.utils.extend(computedObservable, deferEvaluationOverrides); + } + + if (ko.options['deferUpdates']) { + ko.extenders['deferred'](computedObservable, true); + } + + if (DEBUG) { + // #1731 - Aid debugging by exposing the computed's options + computedObservable["_options"] = options; + } + + if (state.disposeWhenNodeIsRemoved) { + // Since this computed is associated with a DOM node, and we don't want to dispose the computed + // until the DOM node is *removed* from the document (as opposed to never having been in the document), + // we'll prevent disposal until "disposeWhen" first returns false. + state.suppressDisposalUntilDisposeWhenReturnsFalse = true; + + // disposeWhenNodeIsRemoved: true can be used to opt into the "only dispose after first false result" + // behaviour even if there's no specific node to watch. In that case, clear the option so we don't try + // to watch for a non-node's disposal. This technique is intended for KO's internal use only and shouldn't + // be documented or used by application code, as it's likely to change in a future version of KO. + if (!state.disposeWhenNodeIsRemoved.nodeType) { + state.disposeWhenNodeIsRemoved = null; + } + } + + // Evaluate, unless sleeping or deferEvaluation is true + if (!state.isSleeping && !options['deferEvaluation']) { + computedObservable.evaluateImmediate(); } - function haveDependenciesChanged() { - var id, dependency; + // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is + // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose). + if (state.disposeWhenNodeIsRemoved && computedObservable.isActive()) { + ko.utils.domNodeDisposal.addDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback = function () { + computedObservable.dispose(); + }); + } + + return computedObservable; +}; + +// Utility function that disposes a given dependencyTracking entry +function computedDisposeDependencyCallback(id, entryToDispose) { + if (entryToDispose !== null && entryToDispose.dispose) { + entryToDispose.dispose(); + } +} + +// This function gets called each time a dependency is detected while evaluating a computed. +// It's factored out as a shared function to avoid creating unnecessary function instances during evaluation. +function computedBeginDependencyDetectionCallback(subscribable, id) { + var computedObservable = this.computedObservable, + state = computedObservable[computedState]; + if (!state.isDisposed) { + if (this.disposalCount && this.disposalCandidates[id]) { + // Don't want to dispose this subscription, as it's still being used + computedObservable.addDependencyTracking(id, subscribable, this.disposalCandidates[id]); + this.disposalCandidates[id] = null; // No need to actually delete the property - disposalCandidates is a transient object anyway + --this.disposalCount; + } else if (!state.dependencyTracking[id]) { + // Brand new subscription - add it + computedObservable.addDependencyTracking(id, subscribable, state.isSleeping ? { _target: subscribable } : computedObservable.subscribeToDependency(subscribable)); + } + // If the observable we've accessed has a pending notification, ensure we get notified of the actual final value (bypass equality checks) + if (subscribable._notificationIsPending) { + subscribable._notifyNextChangeIfValueIsDifferent(); + } + } +} + +var computedFn = { + "equalityComparer": valuesArePrimitiveAndEqual, + getDependenciesCount: function () { + return this[computedState].dependenciesCount; + }, + addDependencyTracking: function (id, target, trackingObj) { + if (this[computedState].pure && target === this) { + throw Error("A 'pure' computed must not be called recursively"); + } + + this[computedState].dependencyTracking[id] = trackingObj; + trackingObj._order = this[computedState].dependenciesCount++; + trackingObj._version = target.getVersion(); + }, + haveDependenciesChanged: function () { + var id, dependency, dependencyTracking = this[computedState].dependencyTracking; for (id in dependencyTracking) { if (dependencyTracking.hasOwnProperty(id)) { dependency = dependencyTracking[id]; - if (dependency._target.hasChanged(dependency._version)) { + if ((this._evalDelayed && dependency._target._notificationIsPending) || dependency._target.hasChanged(dependency._version)) { return true; } } } - } - - function disposeComputed() { - if (!isSleeping && dependencyTracking) { - ko.utils.objectForEach(dependencyTracking, function (id, dependency) { - if (dependency.dispose) - dependency.dispose(); - }); + }, + markDirty: function () { + // Process "dirty" events if we can handle delayed notifications + if (this._evalDelayed && !this[computedState].isBeingEvaluated) { + this._evalDelayed(false /*isChange*/); } - dependencyTracking = null; - _dependenciesCount = 0; - _isDisposed = true; - _needsEvaluation = false; - isSleeping = false; - } - - function evaluatePossiblyAsync() { - var throttleEvaluationTimeout = dependentObservable['throttleEvaluation']; + }, + isActive: function () { + var state = this[computedState]; + return state.isDirty || state.dependenciesCount > 0; + }, + respondToChange: function () { + // Ignore "change" events if we've already scheduled a delayed notification + if (!this._notificationIsPending) { + this.evaluatePossiblyAsync(); + } else if (this[computedState].isDirty) { + this[computedState].isStale = true; + } + }, + subscribeToDependency: function (target) { + if (target._deferUpdates && !this[computedState].disposeWhenNodeIsRemoved) { + var dirtySub = target.subscribe(this.markDirty, this, 'dirty'), + changeSub = target.subscribe(this.respondToChange, this); + return { + _target: target, + dispose: function () { + dirtySub.dispose(); + changeSub.dispose(); + } + }; + } else { + return target.subscribe(this.evaluatePossiblyAsync, this); + } + }, + evaluatePossiblyAsync: function () { + var computedObservable = this, + throttleEvaluationTimeout = computedObservable['throttleEvaluation']; if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { - clearTimeout(evaluationTimeoutInstance); - evaluationTimeoutInstance = setTimeout(function () { - evaluateImmediate(true /*notifyChange*/); + clearTimeout(this[computedState].evaluationTimeoutInstance); + this[computedState].evaluationTimeoutInstance = ko.utils.setTimeout(function () { + computedObservable.evaluateImmediate(true /*notifyChange*/); }, throttleEvaluationTimeout); - } else if (dependentObservable._evalRateLimited) { - dependentObservable._evalRateLimited(); + } else if (computedObservable._evalDelayed) { + computedObservable._evalDelayed(true /*isChange*/); } else { - evaluateImmediate(true /*notifyChange*/); + computedObservable.evaluateImmediate(true /*notifyChange*/); } - } + }, + evaluateImmediate: function (notifyChange) { + var computedObservable = this, + state = computedObservable[computedState], + disposeWhen = state.disposeWhen, + changed = false; - function evaluateImmediate(notifyChange) { - if (_isBeingEvaluated) { + if (state.isBeingEvaluated) { // If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation. // This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost // certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing @@ -1688,297 +2091,262 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva } // Do not evaluate (and possibly capture new dependencies) if disposed - if (_isDisposed) { + if (state.isDisposed) { return; } - if (disposeWhen && disposeWhen()) { - // See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse - if (!_suppressDisposalUntilDisposeWhenReturnsFalse) { - dispose(); + if (state.disposeWhenNodeIsRemoved && !ko.utils.domNodeIsAttachedToDocument(state.disposeWhenNodeIsRemoved) || disposeWhen && disposeWhen()) { + // See comment above about suppressDisposalUntilDisposeWhenReturnsFalse + if (!state.suppressDisposalUntilDisposeWhenReturnsFalse) { + computedObservable.dispose(); return; } } else { // It just did return false, so we can stop suppressing now - _suppressDisposalUntilDisposeWhenReturnsFalse = false; + state.suppressDisposalUntilDisposeWhenReturnsFalse = false; } - _isBeingEvaluated = true; - + state.isBeingEvaluated = true; try { - // 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 = dependencyTracking, - disposalCount = _dependenciesCount, - isInitial = pure ? undefined : !_dependenciesCount; // If we're evaluating when there are no previous dependencies, it must be the first time - - ko.dependencyDetection.begin({ - callback: function(subscribable, id) { - if (!_isDisposed) { - if (disposalCount && disposalCandidates[id]) { - // Don't want to dispose this subscription, as it's still being used - addDependencyTracking(id, subscribable, disposalCandidates[id]); - delete disposalCandidates[id]; - --disposalCount; - } else if (!dependencyTracking[id]) { - // Brand new subscription - add it - addDependencyTracking(id, subscribable, isSleeping ? { _target: subscribable } : subscribable.subscribe(evaluatePossiblyAsync)); - } - } - }, - computed: dependentObservable, - isInitial: isInitial - }); - - dependencyTracking = {}; - _dependenciesCount = 0; - - try { - var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction(); + changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange); + } finally { + state.isBeingEvaluated = false; + } - } finally { - ko.dependencyDetection.end(); + if (!state.dependenciesCount) { + computedObservable.dispose(); + } - // For each subscription no longer being used, remove it from the active subscriptions list and dispose it - if (disposalCount && !isSleeping) { - ko.utils.objectForEach(disposalCandidates, function(id, toDispose) { - if (toDispose.dispose) - toDispose.dispose(); - }); - } + return changed; + }, + evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) { + // This function is really just part of the evaluateImmediate logic. You would never call it from anywhere else. + // Factoring it out into a separate function means it can be independent of the try/catch block in evaluateImmediate, + // which contributes to saving about 40% off the CPU overhead of computed evaluation (on V8 at least). + + var computedObservable = this, + state = computedObservable[computedState], + changed = false; + + // 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 isInitial = state.pure ? undefined : !state.dependenciesCount, // If we're evaluating when there are no previous dependencies, it must be the first time + dependencyDetectionContext = { + computedObservable: computedObservable, + disposalCandidates: state.dependencyTracking, + disposalCount: state.dependenciesCount + }; - _needsEvaluation = false; - } + ko.dependencyDetection.begin({ + callbackTarget: dependencyDetectionContext, + callback: computedBeginDependencyDetectionCallback, + computed: computedObservable, + isInitial: isInitial + }); - if (dependentObservable.isDifferent(_latestValue, newValue)) { - if (!isSleeping) { - notify(_latestValue, "beforeChange"); - } + state.dependencyTracking = {}; + state.dependenciesCount = 0; - _latestValue = newValue; - if (DEBUG) dependentObservable._latestValue = _latestValue; + var newValue = this.evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext); - if (isSleeping) { - dependentObservable.updateVersion(); - } else if (notifyChange) { - notify(_latestValue); - } + if (computedObservable.isDifferent(state.latestValue, newValue)) { + if (!state.isSleeping) { + computedObservable["notifySubscribers"](state.latestValue, "beforeChange"); } - if (isInitial) { - notify(_latestValue, "awake"); - } - } finally { - _isBeingEvaluated = false; - } - - if (!_dependenciesCount) - dispose(); - } + state.latestValue = newValue; + if (DEBUG) computedObservable._latestValue = newValue; - function dependentObservable() { - if (arguments.length > 0) { - if (typeof writeFunction === "function") { - // Writing a value - writeFunction.apply(evaluatorFunctionTarget, arguments); - } 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."); - } - return this; // Permits chained assignments - } else { - // Reading the value - ko.dependencyDetection.registerDependency(dependentObservable); - if (_needsEvaluation || (isSleeping && haveDependenciesChanged())) { - evaluateImmediate(); + if (state.isSleeping) { + computedObservable.updateVersion(); + } else if (notifyChange) { + computedObservable["notifySubscribers"](state.latestValue); } - return _latestValue; - } - } - function peek() { - // Peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set. - if ((_needsEvaluation && !_dependenciesCount) || (isSleeping && haveDependenciesChanged())) { - evaluateImmediate(); + changed = true; } - return _latestValue; - } - - function isActive() { - return _needsEvaluation || _dependenciesCount > 0; - } - function notify(value, event) { - dependentObservable["notifySubscribers"](value, event); - } - - // By here, "options" is always non-null - var writeFunction = options["write"], - disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null, - disposeWhenOption = options["disposeWhen"] || options.disposeWhen, - disposeWhen = disposeWhenOption, - dispose = disposeComputed, - dependencyTracking = {}, - _dependenciesCount = 0, - evaluationTimeoutInstance = null; + if (isInitial) { + computedObservable["notifySubscribers"](state.latestValue, "awake"); + } - if (!evaluatorFunctionTarget) - evaluatorFunctionTarget = options["owner"]; + return changed; + }, + evaluateImmediate_CallReadThenEndDependencyDetection: function (state, dependencyDetectionContext) { + // This function is really part of the evaluateImmediate_CallReadWithDependencyDetection logic. + // You'd never call it from anywhere else. Factoring it out means that evaluateImmediate_CallReadWithDependencyDetection + // can be independent of try/finally blocks, which contributes to saving about 40% off the CPU + // overhead of computed evaluation (on V8 at least). - ko.subscribable.call(dependentObservable); - ko.utils.setPrototypeOfOrExtend(dependentObservable, ko.dependentObservable['fn']); + try { + var readFunction = state.readFunction; + return state.evaluatorFunctionTarget ? readFunction.call(state.evaluatorFunctionTarget) : readFunction(); + } finally { + ko.dependencyDetection.end(); - dependentObservable.peek = peek; - dependentObservable.getDependenciesCount = function () { return _dependenciesCount; }; - dependentObservable.hasWriteFunction = typeof writeFunction === "function"; - dependentObservable.dispose = function () { dispose(); }; - dependentObservable.isActive = isActive; + // For each subscription no longer being used, remove it from the active subscriptions list and dispose it + if (dependencyDetectionContext.disposalCount && !state.isSleeping) { + ko.utils.objectForEach(dependencyDetectionContext.disposalCandidates, computedDisposeDependencyCallback); + } - // Replace the limit function with one that delays evaluation as well. - var originalLimit = dependentObservable.limit; - dependentObservable.limit = function(limitFunction) { - originalLimit.call(dependentObservable, limitFunction); - dependentObservable._evalRateLimited = function() { - dependentObservable._rateLimitedBeforeChange(_latestValue); + state.isStale = state.isDirty = false; + } + }, + peek: function (evaluate) { + // By default, peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set. + // Pass in true to evaluate if needed. + var state = this[computedState]; + if ((state.isDirty && (evaluate || !state.dependenciesCount)) || (state.isSleeping && this.haveDependenciesChanged())) { + this.evaluateImmediate(); + } + return state.latestValue; + }, + limit: function (limitFunction) { + // Override the limit function with one that delays evaluation as well + ko.subscribable['fn'].limit.call(this, limitFunction); + this._evalIfChanged = function () { + if (this[computedState].isStale) { + this.evaluateImmediate(); + } else { + this[computedState].isDirty = false; + } + return this[computedState].latestValue; + }; + this._evalDelayed = function (isChange) { + this._limitBeforeChange(this[computedState].latestValue); - _needsEvaluation = true; // Mark as dirty + // Mark as dirty + this[computedState].isDirty = true; + if (isChange) { + this[computedState].isStale = true; + } - // Pass the observable to the rate-limit code, which will access it when + // Pass the observable to the "limit" code, which will evaluate it when // it's time to do the notification. - dependentObservable._rateLimitedChange(dependentObservable); + this._limitChange(this); + }; + }, + dispose: function () { + var state = this[computedState]; + if (!state.isSleeping && state.dependencyTracking) { + ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { + if (dependency.dispose) + dependency.dispose(); + }); } - }; + if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) { + ko.utils.domNodeDisposal.removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback); + } + state.dependencyTracking = null; + state.dependenciesCount = 0; + state.isDisposed = true; + state.isStale = false; + state.isDirty = false; + state.isSleeping = false; + state.disposeWhenNodeIsRemoved = null; + } +}; - if (options['pure']) { - pure = true; - isSleeping = true; // Starts off sleeping; will awake on the first subscription - dependentObservable.beforeSubscriptionAdd = function (event) { - // If asleep, wake up the computed by subscribing to any dependencies. - if (!_isDisposed && isSleeping && event == 'change') { - isSleeping = false; - if (_needsEvaluation || haveDependenciesChanged()) { - dependencyTracking = null; - _dependenciesCount = 0; - _needsEvaluation = true; - evaluateImmediate(); - } else { - // First put the dependencies in order - var dependeciesOrder = []; - ko.utils.objectForEach(dependencyTracking, function (id, dependency) { - dependeciesOrder[dependency._order] = id; - }); - // Next, subscribe to each one - ko.utils.arrayForEach(dependeciesOrder, function(id, order) { - var dependency = dependencyTracking[id], - subscription = dependency._target.subscribe(evaluatePossiblyAsync); - subscription._order = order; - subscription._version = dependency._version; - dependencyTracking[id] = subscription; - }); - } - if (!_isDisposed) { // test since evaluating could trigger disposal - notify(_latestValue, "awake"); +var pureComputedOverrides = { + beforeSubscriptionAdd: function (event) { + // If asleep, wake up the computed by subscribing to any dependencies. + var computedObservable = this, + state = computedObservable[computedState]; + if (!state.isDisposed && state.isSleeping && event == 'change') { + state.isSleeping = false; + if (state.isStale || computedObservable.haveDependenciesChanged()) { + state.dependencyTracking = null; + state.dependenciesCount = 0; + if (computedObservable.evaluateImmediate()) { + computedObservable.updateVersion(); } - } - }; - - dependentObservable.afterSubscriptionRemove = function (event) { - if (!_isDisposed && event == 'change' && !dependentObservable.hasSubscriptionsForEvent('change')) { - ko.utils.objectForEach(dependencyTracking, function (id, dependency) { - if (dependency.dispose) { - dependencyTracking[id] = { - _target: dependency._target, - _order: dependency._order, - _version: dependency._version - }; - dependency.dispose(); - } + } else { + // First put the dependencies in order + var dependeciesOrder = []; + ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { + dependeciesOrder[dependency._order] = id; + }); + // Next, subscribe to each one + ko.utils.arrayForEach(dependeciesOrder, function (id, order) { + var dependency = state.dependencyTracking[id], + subscription = computedObservable.subscribeToDependency(dependency._target); + subscription._order = order; + subscription._version = dependency._version; + state.dependencyTracking[id] = subscription; }); - isSleeping = true; - notify(undefined, "asleep"); } - }; - + if (!state.isDisposed) { // test since evaluating could trigger disposal + computedObservable["notifySubscribers"](state.latestValue, "awake"); + } + } + }, + afterSubscriptionRemove: function (event) { + var state = this[computedState]; + if (!state.isDisposed && event == 'change' && !this.hasSubscriptionsForEvent('change')) { + ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { + if (dependency.dispose) { + state.dependencyTracking[id] = { + _target: dependency._target, + _order: dependency._order, + _version: dependency._version + }; + dependency.dispose(); + } + }); + state.isSleeping = true; + this["notifySubscribers"](undefined, "asleep"); + } + }, + getVersion: function () { // Because a pure computed is not automatically updated while it is sleeping, we can't // simply return the version number. Instead, we check if any of the dependencies have // changed and conditionally re-evaluate the computed observable. - dependentObservable._originalGetVersion = dependentObservable.getVersion; - dependentObservable.getVersion = function () { - if (isSleeping && (_needsEvaluation || haveDependenciesChanged())) { - evaluateImmediate(); - } - return dependentObservable._originalGetVersion(); - }; - } else if (options['deferEvaluation']) { - // This will force a computed with deferEvaluation to evaluate when the first subscriptions is registered. - dependentObservable.beforeSubscriptionAdd = function (event) { - if (event == 'change' || event == 'beforeChange') { - peek(); - } + var state = this[computedState]; + if (state.isSleeping && (state.isStale || this.haveDependenciesChanged())) { + this.evaluateImmediate(); } + return ko.subscribable['fn'].getVersion.call(this); } +}; - ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek); - ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose); - ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive); - ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount); - - // Add a "disposeWhen" callback that, on each evaluation, disposes if the node was removed without using ko.removeNode. - if (disposeWhenNodeIsRemoved) { - // Since this computed is associated with a DOM node, and we don't want to dispose the computed - // until the DOM node is *removed* from the document (as opposed to never having been in the document), - // we'll prevent disposal until "disposeWhen" first returns false. - _suppressDisposalUntilDisposeWhenReturnsFalse = true; - - // Only watch for the node's disposal if the value really is a node. It might not be, - // e.g., { disposeWhenNodeIsRemoved: true } can be used to opt into the "only dispose - // after first false result" behaviour even if there's no specific node to watch. This - // technique is intended for KO's internal use only and shouldn't be documented or used - // by application code, as it's likely to change in a future version of KO. - if (disposeWhenNodeIsRemoved.nodeType) { - disposeWhen = function () { - return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || (disposeWhenOption && disposeWhenOption()); - }; +var deferEvaluationOverrides = { + beforeSubscriptionAdd: function (event) { + // This will force a computed with deferEvaluation to evaluate when the first subscription is registered. + if (event == 'change' || event == 'beforeChange') { + this.peek(); } } - - // Evaluate, unless sleeping or deferEvaluation is true - if (!isSleeping && !options['deferEvaluation']) - evaluateImmediate(); - - // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is - // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose). - if (disposeWhenNodeIsRemoved && isActive() && disposeWhenNodeIsRemoved.nodeType) { - dispose = function() { - ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose); - disposeComputed(); - }; - ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose); - } - - return dependentObservable; }; -ko.isComputed = function(instance) { - return ko.hasPrototype(instance, ko.dependentObservable); -}; +// Note that for browsers that don't support proto assignment, the +// inheritance chain is created manually in the ko.computed constructor +if (ko.utils.canSetPrototype) { + ko.utils.setPrototypeOf(computedFn, ko.subscribable['fn']); +} +// Set the proto chain values for ko.hasPrototype var protoProp = ko.observable.protoProperty; // == "__ko_proto__" -ko.dependentObservable[protoProp] = ko.observable; +ko.computed[protoProp] = ko.observable; +computedFn[protoProp] = ko.computed; -ko.dependentObservable['fn'] = { - "equalityComparer": valuesArePrimitiveAndEqual +ko.isComputed = function (instance) { + return ko.hasPrototype(instance, ko.computed); }; -ko.dependentObservable['fn'][protoProp] = ko.dependentObservable; -// Note that for browsers that don't support proto assignment, the -// inheritance chain is created manually in the ko.dependentObservable constructor -if (ko.utils.canSetPrototype) { - ko.utils.setPrototypeOf(ko.dependentObservable['fn'], ko.subscribable['fn']); -} +ko.isPureComputed = function (instance) { + return ko.hasPrototype(instance, ko.computed) + && instance[computedState] && instance[computedState].pure; +}; -ko.exportSymbol('dependentObservable', ko.dependentObservable); -ko.exportSymbol('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable" +ko.exportSymbol('computed', ko.computed); +ko.exportSymbol('dependentObservable', ko.computed); // export ko.dependentObservable for backwards compatibility (1.x) ko.exportSymbol('isComputed', ko.isComputed); +ko.exportSymbol('isPureComputed', ko.isPureComputed); +ko.exportSymbol('computed.fn', computedFn); +ko.exportProperty(computedFn, 'peek', computedFn.peek); +ko.exportProperty(computedFn, 'dispose', computedFn.dispose); +ko.exportProperty(computedFn, 'isActive', computedFn.isActive); +ko.exportProperty(computedFn, 'getDependenciesCount', computedFn.getDependenciesCount); ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget) { if (typeof evaluatorFunctionOrOptions === 'function') { @@ -2016,7 +2384,7 @@ ko.exportSymbol('pureComputed', ko.pureComputed); visitedObjects = visitedObjects || new objectLookup(); rootObject = mapInputCallback(rootObject); - var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean)); + var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof RegExp)) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean)); if (!canHaveProperties) return rootObject; @@ -2629,14 +2997,16 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); (function () { ko.bindingHandlers = {}; - // The following element types will not be recursed into during binding. In the future, we - // may consider adding