zoukankan      html  css  js  c++  java
  • knockout的监控数组实现

    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了。

  • 相关阅读:
    循环冗余检验 (CRC) 算法原理
    工具分享——将C#文档注释生成.chm帮助文档
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/3579376.html
Copyright © 2011-2022 走看看