knockout应该是博客园群体中使用最广的MVVM框架,但鲜有介绍其监控数组的实现。最近试图升级avalon的监控数组,决定好好研究它一番,看有没有可借鉴之处。
ko.observableArray = function (initialValues) { initialValues = initialValues || []; if
( typeof initialValues != ‘object‘
|| !( ‘length‘
in initialValues)) throw
new Error( "The argument passed when initializing an observable array must be an array, or null, or undefined." ); var
result = ko.observable(initialValues); ko.utils.extend(result, ko.observableArray[ ‘fn‘ ]); return
result.extend({ ‘trackArrayChanges‘ : true }); }; |
这是knockout监控数组的工厂方法,不需要使用new关键字,直接转换一个普通数组为一个监控数组。你也可以什么也不会,得到一个空的监控数组。
var myObservableArray = ko.observableArray(); // Initially an empty array myObservableArray.push( ‘Some value‘ ); // Adds the value and notifies obs // This observable array initially contains three objects var anotherObservableArray = ko.observableArray([ { name: "Bungle" , type: "Bear"
}, { name: "George" , type: "Hippo"
}, { name: "Zippy" , type: "Unknown"
} ]); console.log( typeof
anotherObservableArray) //function |
虽说是监控数组,但它的类型其实是一个函数。这正是knockout令人不爽的地方,将原本是字符串,数字,布尔,数组等东西都转换为函数才行使用。
这里有一个ko.utils.extend方法,比不上jQuery的同名方法,只是一个浅拷贝,将一个对象的属性循环复制到另一个之上。
extend: function (target, source) { if
(source) { for
( var prop in source) { if
(source.hasOwnProperty(prop)) { target[prop] = source[prop]; } } } return
target; }, |
result 是要返回的函数,它会被挂上许多方法与属性。首先是 ko.observableArray[‘fn‘]扩展包,第二个扩展其实可以简化为
result.trackArrayChanges = true |
我们来看一下 ko.observableArray[‘fn‘]扩展包,其中最难的是pop,push,shift等方法的实现
ko.observableArray[ ‘fn‘ ] = { ‘remove‘ : function (valueOrPredicate) { //值可以是原始数组或一个监控函数 var
underlyingArray = this .peek(); //得到原始数组 var
removedValues = []; var
predicate = typeof
valueOrPredicate == "function"
&& !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return
value === valueOrPredicate; }; //确保转换为一个函数 for
( var i = 0; i < underlyingArray.length; i++) { var
value = underlyingArray[i]; if
(predicate(value)) { if
(removedValues.length === 0) { this .valueWillMutate(); //开始变动 } removedValues.push(value); underlyingArray.splice(i, 1); //移除元素 i--; } } if
(removedValues.length) { //如果不为空,说明发生移除,就调用valueHasMutated this .valueHasMutated(); } return
removedValues; //返回被移除的元素 }, ‘removeAll‘ : function (arrayOfValues) { // If you passed zero args, we remove everything if
(arrayOfValues === undefined) { //如果什么也不传,则清空数组 var
underlyingArray = this .peek(); var
allValues = underlyingArray.slice(0); this .valueWillMutate(); underlyingArray.splice(0, underlyingArray.length); this .valueHasMutated(); return
allValues; } //如果是传入空字符串,null, NaN if
(!arrayOfValues) return
[]; return
this [ ‘remove‘ ]( function (value) { //否则调用上面的remove方法 return
ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; }); }, ‘destroy‘ : function (valueOrPredicate) { //remove方法的优化版,不立即移除元素,只是标记一下 var
underlyingArray = this .peek(); var
predicate = typeof
valueOrPredicate == "function"
&& !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return
value === valueOrPredicate; }; this .valueWillMutate(); for
( var i = underlyingArray.length - 1; i >= 0; i--) { var
value = underlyingArray[i]; if
(predicate(value)) underlyingArray[i][ "_destroy" ] = true ; } this .valueHasMutated(); }, ‘destroyAll‘ : function (arrayOfValues) { //removeAll方法的优化版,不立即移除元素,只是标记一下 if
(arrayOfValues === undefined) //不传就全部标记为destroy return
this [ ‘destroy‘ ]( function () { return
true }); // If you passed an arg, we interpret it as an array of entries to destroy if
(!arrayOfValues) return
[]; return
this [ ‘destroy‘ ]( function (value) { return
ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; }); }, ‘indexOf‘ : function (item) { //返回索引值 var
underlyingArray = this (); return
ko.utils.arrayIndexOf(underlyingArray, item); }, ‘replace‘ : function (oldItem, newItem) { //替换某一位置的元素 var
index = this [ ‘indexOf‘ ](oldItem); if
(index >= 0) { this .valueWillMutate(); this .peek()[index] = newItem; this .valueHasMutated(); } } }; //添加一系列与原生数组同名的方法 ko.utils.arrayForEach([ "pop" , "push" , "reverse" , "shift" , "sort" , "splice" , "unshift" ], function (methodName) { ko.observableArray[ ‘fn‘ ][methodName] = function () { var
underlyingArray = this .peek(); this .valueWillMutate(); this .cacheDiffForKnownOperation(underlyingArray, methodName, arguments); var
methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments); this .valueHasMutated(); return
methodCallResult; }; }); //返回一个真正的数组 ko.utils.arrayForEach([ "slice" ], function (methodName) { ko.observableArray[ ‘fn‘ ][methodName] = function () { var
underlyingArray = this (); return
underlyingArray[methodName].apply(underlyingArray, arguments); }; }); |
cacheDiffForKnownOperation 会记录如何对元素进行操作
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; }; }; ko.utils.findMovesInArrayComparison = function (left, right, limitFailedCompares) { if
(left.length && right.length) { var
failedCompares, l, r, leftItem, rightItem; for
(failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) { for
(r = 0; rightItem = right[r]; ++r) { if
(leftItem[ ‘value‘ ] === rightItem[ ‘value‘ ]) { leftItem[ ‘moved‘ ] = rightItem[ ‘index‘ ]; rightItem[ ‘moved‘ ] = leftItem[ ‘index‘ ]; right.splice(r, 1); // This item is marked as moved; so remove it from right list failedCompares = r = 0; // Reset failed compares count because we‘re checking for consecutive failures break ; } } failedCompares += r; } } }; |
但这里没有sort, reverse方法的处理,并且它是如何操作DOM呢?由于它很早就转换为监控函数,但用户调用这些方法时,它就会在内部调用一个叫getChanges的方法
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, {‘sparse‘: true }); } return
cachedDiff; } |
里面有一个compareArrays方法,会计算出如何用最少的步骤实现DOM的改动,从而减少reflow。
ko.utils.compareArrays = ( function () { var
statusNotInOld = ‘added‘ , statusNotInNew = ‘deleted‘ ; // Simple calculation based on Levenshtein distance. function
compareArrays(oldArray, newArray, options) { // For backward compatibility, if the third arg is actually a bool, interpret // it as the old parameter ‘dontLimitMoves‘. Newer code should use { dontLimitMoves: true }. options = ( typeof
options === ‘boolean‘ ) ? { ‘dontLimitMoves‘ : options} : (options || {}); oldArray = oldArray || []; newArray = newArray || []; if
(oldArray.length <= newArray.length) return
compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options); else return
compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options); } function
compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, options) { var
myMin = Math.min, myMax = Math.max, editDistanceMatrix = [], smlIndex, smlIndexMax = smlArray.length, bigIndex, bigIndexMax = bigArray.length, compareRange = (bigIndexMax - smlIndexMax) || 1, maxDistance = smlIndexMax + bigIndexMax + 1, thisRow, lastRow, bigIndexMaxForRow, bigIndexMinForRow; for
(smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) { lastRow = thisRow; editDistanceMatrix.push(thisRow = []); bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange); bigIndexMinForRow = myMax(0, smlIndex - 1); for
(bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) { if
(!bigIndex) thisRow[bigIndex] = smlIndex + 1; else
if (!smlIndex) // Top row - transform empty array into new array via additions thisRow[bigIndex] = bigIndex + 1; else
if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1]) thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit) else
{ var
northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion) var
westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition) thisRow[bigIndex] = myMin(northDistance, westDistance) + 1; } } } var
editScript = [], meMinusOne, notInSml = [], notInBig = []; for
(smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex; ) { meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1; if
(bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex - 1]) { notInSml.push(editScript[editScript.length] = { // added ‘status‘ : statusNotInSml, ‘value‘ : bigArray[--bigIndex], ‘index‘ : bigIndex}); } else
if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) { notInBig.push(editScript[editScript.length] = { // deleted ‘status‘ : statusNotInBig, ‘value‘ : smlArray[--smlIndex], ‘index‘ : smlIndex}); } else
{ --bigIndex; --smlIndex; if
(!options[ ‘sparse‘ ]) { editScript.push({ ‘status‘ : "retained" , ‘value‘ : bigArray[bigIndex]}); } } } // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of // smlIndexMax keeps the time complexity of this algorithm linear. ko.utils.findMovesInArrayComparison(notInSml, notInBig, smlIndexMax * 10); return
editScript.reverse(); } return
compareArrays; })(); |
最后会跑到setDomNodeChildrenFromArrayMapping 里面执行相关的操作
for ( var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) { movedIndex = editScriptItem[ ‘moved‘ ]; switch
(editScriptItem[ ‘status‘ ]) { case
"deleted" : if
(movedIndex === undefined) { mapData = lastMappingResult[lastMappingResultIndex]; // Stop tracking changes to the mapping for these nodes if
(mapData.dependentObservable) mapData.dependentObservable.dispose(); // Queue these nodes for later removal nodesToDelete.push.apply(nodesToDelete, ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode)); if
(options[ ‘beforeRemove‘ ]) { itemsForBeforeRemoveCallbacks[i] = mapData; itemsToProcess.push(mapData); } } lastMappingResultIndex++; break ; case
"retained" : itemMovedOrRetained(i, lastMappingResultIndex++); break ; case
"added" : if
(movedIndex !== undefined) { itemMovedOrRetained(i, movedIndex); } else
{ mapData = {arrayEntry: editScriptItem[ ‘value‘ ], indexObservable: ko.observable(newMappingResultIndex++)}; newMappingResult.push(mapData); itemsToProcess.push(mapData); if
(!isFirstExecution) itemsForAfterAddCallbacks[i] = mapData; } break ; } } //下面是各种回调操作 |
整个实现比avalon复杂得不是一点半点啊,这是太迷信算法的下场。其实像shift, unshift, pop, push, splice等方法,我们一开始就能确定如何增删,不用跑到compareArrays 里面,最麻烦的sort, reverse方法,也可以通过将父节点移出DOM树,排好再插回去,就能避免reflow了。
如果您觉得这文章对您有帮助,可以打赏点钱给我,鼓励我继续写博,我的支付宝
knockout的监控数组实现,布布扣,bubuko.com
原文:http://www.cnblogs.com/rubylouvre/p/3579376.html