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 = 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 

但这里没有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 

最后会跑到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了。

相关文章: