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 to this list, because such elements' contents are always
- // intended to be bound in a different context from where they appear in the document.
+ // The following element types will not be recursed into during binding.
var bindingDoesNotRecurseIntoElementTypes = {
// Don't want bindings that operate on text nodes to mutate