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) {//如果不为空,说明发生移除,就调用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;}}};

但这里没有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);elsereturn 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 additionsthisRow[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 nodesif (mapData.dependentObservable)mapData.dependentObservable.dispose();// Queue these nodes for later removalnodesToDelete.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的监控数组实现 - 司徒正美相关推荐

  1. 迷你MVVM框架 avalonjs 入门教程(司徒正美)

    迷你MVVM框架 avalonjs 入门教程 关于AvalonJs 开始的例子 扫描 视图模型 数据模型 绑定属性与动态模板 作用域绑定(ms-controller, ms-important) 模板 ...

  2. 如何挑选适合的前端框架(去哪儿网前端架构师司徒正美)

    前端框架不断推新,众多IT企业都面临着"如何选择框架","是否需要再造轮子"的抉择.去哪儿网前端架构师司徒正美分析了各主流行框架优劣点.适用场景,并针对不同规模 ...

  3. javascript 异步编程二(转载 from 司徒正美)

    好像有这么一句名言--"每一个优雅的接口,背后都有一个龌龊的实现".最明显的例子,jQuery.之所以弄得这么复杂,因为它本来就是那复杂.虽然有些实现相对简明些,那是它们的兼容程度 ...

  4. 送!司徒正美写给前端开发者的算法书

    "每天学习一点点算法",相信很多被算法"折磨"过的人都曾立下这样的Flag,并向算法发出一轮又一轮的进攻. 这也是司徒正美老师博客园首页上的一句话.在那上面,他 ...

  5. JAVASCRIPT 正则表达式学习--基础与零宽断言(转自司徒正美)

    元字符 ( [ { \ ^ $ | ) ? * + . 预定义的特殊字符 字符 正则 描述 \t /\t/ 制表符 \n /\n/ 制表符 \r /\r/ 回车符 \f /\f/ 换页符 \a /\a ...

  6. 司徒正美写给前端开发者的算法书(文末抽奖送书)

    "每天学习一点点算法",相信很多被算法"折磨"过的人都曾立下这样的Flag,并向算法发出一轮又一轮的进攻. 这也是司徒正美老师博客园首页上的一句话.在那上面,他 ...

  7. 司徒正美写给前端开发者的算法书

    "每天学习一点点算法",相信很多被算法"折磨"过的人都曾立下这样的Flag,并向算法发出一轮又一轮的进攻. 这也是司徒正美老师博客园首页上的一句话.在那上面,他 ...

  8. javascript 45种缓动效果BY司徒正美

    javascript 45种缓动效果 参数 类型 说明 el element 必需,为页面元素 begin number 必需,开始的位置 change number 必需,要移动的距离 durati ...

  9. 在知乎看到一篇关于JavaScript书籍进阶的回答(作者:司徒正美)

    所谓进阶,就是不再为语法烦恼,开始向繁杂而精彩的DOM.BOM世界进军. 掌握各大浏览器提供的底层DOM.BOM API,及了解它们之间的差异,如何检测它们是否支持,如果屏蔽它们之间的差异性,如何选用 ...

  10. NSA监控全球反病毒厂商 英美除外

    本文讲的是 NSA监控全球反病毒厂商 英美除外,自由斯诺登网(Edwardsnowden.com)最新披露了一份美国NSA内部文件<An Easy Win:Using SIGINT to Lea ...

最新文章

  1. leetcode算法题--合并两个有序数组
  2. 深度学(deep learning)基础-神经网络简易教程
  3. Tomcat connector 实现原理
  4. centos 对已有卷扩容_centos LVM扩容 添加磁盘
  5. MongoDB 分片
  6. docker镜像创建与优化
  7. c#获取文件夹路径(转载)
  8. 软件测试---弹出窗口
  9. java 解锁关闭文件占用_程序员:Java文件锁定、解锁和其它NIO操作
  10. DB2数据库问题总结
  11. lzg_ad:EWF启用常见问题及解决方案
  12. 适合财务人员的财务报表分析软件有哪些?
  13. Surface Pro的MicroSDHC卡测速
  14. Flutter 实现光影变换的立体旋转效果
  15. tex排版,论文中图片转为eps格式,(pdf,visio转pes)eps图显示不完全
  16. caffe函数入口caffe.cpp详解
  17. 20T数据迁移经验:手把手教你群晖NAS数据迁移,黑裙晖通用!
  18. GhostXP SP3 统一会员纯净版 V2.1
  19. 关于透明桌面相框图片不能显示的问题
  20. iOS系统字体如何使用

热门文章

  1. GLSL-Compute Shader
  2. 广东省取消职称英语和计算机,职称评审!这些省份短期内不会取消职称英语、计算机!...
  3. erdas几何校正_实验一 ERDAS介绍与图像几何校正
  4. AutoCAD二次开发基本操作命令
  5. 机房服务器配置方案文件,机房搬迁实施方案模版
  6. WebService-服务端与客户端
  7. 前端使用(久派)高拍仪进行拍照上传
  8. OA软件办公用品分类设置,实现办公用品分类透明化
  9. ORA-12162: TNS:net service name is incorrectly spe
  10. java list 冒泡_JAVA List 排序 冒泡排序