Welcome 微信登录

首页 / 脚本样式 / JavaScript / knockout的监控数组实现

knockout的监控数组实现2015-06-19knockout应该是博客园群体中使用最广的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 arraymyObservableArray.push("Some value");// Adds the value and notifies obs// This observable array initially contains three objectsvar 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) {//如果不为空,说明发生移除,就调用valueHasMutatedthis.valueHasMutated();}return removedValues;//返回被移除的元素},"removeAll": function(arrayOfValues) {// If you passed zero args, we remove everythingif (arrayOfValues === undefined) {//如果什么也不传,则清空数组var underlyingArray = this.peek();var allValues = underlyingArray.slice(0);this.valueWillMutate();underlyingArray.splice(0, underlyingArray.length);this.valueHasMutated();return allValues;}//如果是传入空字符串,null, NaNif (!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)//不传就全部标记为destroyreturn this["destroy"](function() {return true});// If you passed an arg, we interpret it as an array of entries to destroyif (!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/splicevar 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 listfailedCompares = r = 0; // Reset failed compares count because we"re checking for consecutive failuresbreak;}}failedCompares += r;}}};