var arrayChangeEventName = 'arrayChange';
ko.extenders['trackArrayChanges'] = function(target) {
    // Only modify the target observable once
    if (target.cacheDiffForKnownOperation) {
        return;
    }
    var trackingChanges = false,
        cachedDiff = null,
        underlyingSubscribeFunction = target.subscribe;

    // Intercept "subscribe" calls, and for array change events, ensure change tracking is enabled
    target.subscribe = target['subscribe'] = function(callback, callbackTarget, event) {
        if (event === arrayChangeEventName) {
            trackChanges();
        }
        return underlyingSubscribeFunction.apply(this, arguments);
    };

    function trackChanges() {
        // Calling 'trackChanges' multiple times is the same as calling it once
        if (trackingChanges) {
            return;
        }

        trackingChanges = true;

        // Each time the array changes value, capture a clone so that on the next
        // change it's possible to produce a diff
        var previousContents = [].concat(target.peek() || []);
        cachedDiff = null;
        target.subscribe(function(currentContents) {
            // Make a copy of the current contents and ensure it's an array
            currentContents = [].concat(currentContents || []);

            // Compute the diff and issue notifications, but only if someone is listening
            if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
                var changes = getChanges(previousContents, currentContents);
                target['notifySubscribers'](changes, arrayChangeEventName);
            }

            // Eliminate references to the old, removed items, so they can be GCed
            previousContents = currentContents;
            cachedDiff = null;
        });
    }

    function getChanges(previousContents, currentContents) {
        // We try to re-use cached diffs.
        if (!cachedDiff) {
            cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true });
        }

        return cachedDiff;
    }

    target.cacheDiffForKnownOperation = function(rawArray, operationName, args) {
        // Only run if we're currently tracking changes for this observable array
        if (!trackingChanges) {
            return;
        }
        var diff = [],
            arrayLength = rawArray.length,
            argsLength = args.length,
            offset = 0;

        function pushDiff(status, value, index) {
            diff.push({ 'status': status, 'value': value, 'index': index });
        }
        switch (operationName) {
            case 'push':
                offset = arrayLength;
            case 'unshift':
                for (var index = 0; index < argsLength; index++) {
                    pushDiff('added', args[index], offset + index);
                }
                break;

            case 'pop':
                offset = arrayLength - 1;
            case 'shift':
                if (arrayLength) {
                    pushDiff('deleted', rawArray[offset], offset);
                }
                break;

            case 'splice':
                // Negative start index means 'from end of array'. After that we clamp to [0...arrayLength].
                // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
                var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
                    endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
                    endAddIndex = startIndex + argsLength - 2,
                    endIndex = Math.max(endDeleteIndex, endAddIndex);
                for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
                    if (index < endDeleteIndex)
                        pushDiff('deleted', rawArray[index], index);
                    if (index < endAddIndex)
                        pushDiff('added', args[argsIndex], index);
                }
                break;

            default:
                return;
        }
        cachedDiff = diff;
    };
};

@@ -263,16 +263,50 @@
 = null,%0A
+        pendingNotifications = 0,%0A
         
@@ -351,16 +351,117 @@
 ubscribe
+,%0A        underlyingNotifySubscribersFunction = target%5B%22notifySubscribers%22%5D,%0A        previousContents
 ;%0A%0A    /
@@ -1007,16 +1007,364 @@
  true;%0A%0A
+        // Intercept %22notifySubscribers%22 to track how many times it was called.%0A        target%5B%22notifySubscribers%22%5D = function(valueToNotify, event) %7B%0A            if (!event %7C%7C event === defaultEvent) %7B%0A                ++pendingNotifications;%0A            %7D%0A            return underlyingNotifySubscribersFunction.apply(this, arguments);%0A        %7D;%0A%0A
         
@@ -1495,20 +1495,16 @@
         
-var 
 previous
@@ -2252,32 +2252,70 @@
 hedDiff = null;%0A
+            pendingNotifications = 0;%0A
         %7D);%0A    
@@ -2443,16 +2443,44 @@
 chedDiff
+ %7C%7C pendingNotifications %3E 1
 ) %7B%0A    
@@ -2784,24 +2784,88 @@
 vable array%0A
+        // and there aren't any pending deferred notifications.%0A
         if (
@@ -2872,32 +2872,56 @@
 !trackingChanges
+ %7C%7C pendingNotifications
 ) %7B%0A            

Use compareArrays instead of the optimized diff when multiple changes result in a notification. This is for compatibility with https://github.com/mbest/knockout-deferred-updates