-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
observableArray.changeTracking.js
152 lines (136 loc) · 6.57 KB
/
observableArray.changeTracking.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
var arrayChangeEventName = 'arrayChange';
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;
}
var trackingChanges = false,
cachedDiff = null,
arrayChangeSubscription,
pendingNotifications = 0,
underlyingNotifySubscribersFunction,
underlyingBeforeSubscriptionAddFunction = target.beforeSubscriptionAdd,
underlyingAfterSubscriptionRemoveFunction = target.afterSubscriptionRemove;
// Watch "subscribe" calls, and for array change events, ensure change tracking is enabled
target.beforeSubscriptionAdd = function (event) {
if (underlyingBeforeSubscriptionAddFunction)
underlyingBeforeSubscriptionAddFunction.call(target, event);
if (event === arrayChangeEventName) {
trackChanges();
}
};
// Watch "dispose" calls, and for array change events, ensure change tracking is disabled when all are disposed
target.afterSubscriptionRemove = function (event) {
if (underlyingAfterSubscriptionRemoveFunction)
underlyingAfterSubscriptionRemoveFunction.call(target, event);
if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) {
if (underlyingNotifySubscribersFunction) {
target['notifySubscribers'] = underlyingNotifySubscribersFunction;
underlyingNotifySubscribersFunction = undefined;
}
if (arrayChangeSubscription)
arrayChangeSubscription.dispose();
arrayChangeSubscription = null;
trackingChanges = false;
}
};
function trackChanges() {
// Calling 'trackChanges' multiple times is the same as calling it once
if (trackingChanges) {
return;
}
trackingChanges = true;
// Intercept "notifySubscribers" to track how many times it was called.
underlyingNotifySubscribersFunction = target['notifySubscribers'];
target['notifySubscribers'] = function(valueToNotify, event) {
if (!event || event === defaultEvent) {
++pendingNotifications;
}
return underlyingNotifySubscribersFunction.apply(this, arguments);
};
// 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;
arrayChangeSubscription = 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);
}
// Eliminate references to the old, removed items, so they can be GCed
previousContents = currentContents;
cachedDiff = null;
pendingNotifications = 0;
if (changes && changes.length) {
target['notifySubscribers'](changes, arrayChangeEventName);
}
});
}
function getChanges(previousContents, currentContents) {
// We try to re-use cached diffs.
// The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
// 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, target.compareArrayOptions);
}
return cachedDiff;
}
target.cacheDiffForKnownOperation = function(rawArray, operationName, args) {
// Only run if we're currently tracking changes for this observable array
// and there aren't any pending deferred notifications.
if (!trackingChanges || pendingNotifications) {
return;
}
var diff = [],
arrayLength = rawArray.length,
argsLength = args.length,
offset = 0;
function pushDiff(status, value, index) {
return diff[diff.length] = { '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),
additions = [], deletions = [];
for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
if (index < endDeleteIndex)
deletions.push(pushDiff('deleted', rawArray[index], index));
if (index < endAddIndex)
additions.push(pushDiff('added', args[argsIndex], index));
}
ko.utils.findMovesInArrayComparison(deletions, additions);
break;
default:
return;
}
cachedDiff = diff;
};
};