JavaScript 中對大量數據的多重過濾

全部代碼使用 ES2015 語法,須要 ES5 語法的能夠用 Babel - Try it out 或者 TypeScript Playground 翻譯。javascript

問題提出

今天有朋友問我一個問題,前端經過 Ajax 從後端取得了大量的數據,須要根據一些條件過濾,過濾的方法是這樣的:html

class Filter {
    filterA(s) {
        let data = this.filterData || this.data;
        this.filterData = data.filter(m => m.a === s);
    }
    
    filterB(s) {
        let data = this.filterData || this.data;
        this.filterData = data.filter(m => m.b === s);
    }
}

如今迷糊了,以爲這樣處理數據不對,可是又不知道該怎麼處理。前端

發現問題

問題就在過濾上,這樣當然能夠實現多重過濾(先調用 filterA() 再調用 filterB() 就能夠實現),可是這個過濾是不可逆的。假如過濾過程是這樣:java

f.filterA("a1");
f.filterB("b1");
f.filterA("a2");

原本是但願按 "a1""b1" 過濾了數據以後,再修改第一個條件爲 "a2",但結果卻成了空集。typescript

解決問題

發現了問題,就針對性的解決。這個問題既然是由於過濾過程不可逆形成的,那每次都直接從 this.data 開始過濾,而不是從 this.filterData 開始過濾,就能解決問題。若是要這樣作,就須要將選擇的過濾條件先記錄下來。後端

記錄過濾條件

用一個列表記錄過濾條件固然是可行的,可是注意對同一個條件的兩次過濾是互斥的,只能保留最後一個,因此應該用 HashMap 更爲合適。bash

class Filter {
    constructor() {
        this.filters = {};
    }

    set(key, filter) {
        this.filters[key] = filter;
    }

    getFilters() {
        return Object.keys(this.filters).map(key => this.filters[key]);
    }
}

這種狀況下,像上面的過程表示爲babel

f.set("A", m => m.a === "a1");
f.set("B", m => m.b === "b1");
f.set("A", m => m.a === "a1");

let filters = f.getFilters();  // length === 2;

上面第 3 句設置的 filter 覆蓋了第 1 句設置的那個。如今再用最後取得的 filters 依次來過濾原數據 this.data,就能獲得正確的結果。工具

有人會以爲 getFilters() 返回的列表不是按 set 的順序的——的確,這是 HashMap 的特色,無序。不過對於簡單條件的判斷,無論誰先誰後,結果是同樣的。可是對於一些複合條件判斷,就可能會有影響。this

確實須要的話,能夠經過 array 代替 map 來解決一下順序的問題,但這樣查找效率會下降(線性查找)。若是還想解決查找效率的問題,能夠用 array + map 來處理。這裏就很少說了。

過濾

實際上在使用的時候,每次都 getFilter() 再用一個循環來處理確實比較慢。既然 data 都封裝成 Filter 中,能夠考慮直接給一個 filter() 方法來做爲過濾接口。

class Filter {
    filter() {
        let data = this.data;
        for (let f of this.getFilters()) {
            data = data.filter(f);
        }
        return data;
    }
}

不過這樣我以爲效率不太好,尤爲是對大量數據的時候。不妨利用一下 lodash 的延遲處理過程。

利用 lodash 的延遲處理

filter() {
    let chain = _(this.data);
    for (let f of this.getFilters()) {
        chain = chain.filter(f);
    }
    return chain.value();
}

lodash 在數據大於 200 的時候會啓用延遲處理過程,也就是說,它會處理成一個循環中依次調用每個 filter,而不是對每個 filter 進行一次循環。

延遲處理和非延遲處理經過下圖能夠看出來區別。非延遲處理總共會進行 n(這裏 n = 3) 次大循環,產生 n - 1 箇中間結果。而延遲處理只會進行一次大循環,沒有中間結果產生。

clipboard.png

不過說實在的,我不太喜歡爲了一點小事多加載一個庫,因此乾脆本身作個簡單的實現

本身實現延遲處理

filter() {
    const filters = this.getFilters();
    return data.filter(m => {
        for (let f of filters) {
            // 若是某個 filter 已經把它過濾掉了,也不用再用後面的 filter 來判斷了
            if (!f(m)) {
                return false;
            }
        }
        return true;
    });
}

裏面的 for 循環還能夠用 Array.prototype.every 來簡化:

filter() {
    const filters = this.getFilters();
    return data.filter(m => {
        return filters.every(f => f(m));
    });
}

數據過濾其實並非多複雜的事情,只要把思路理清楚,搞明白什麼數據是須要保留的,什麼數據是臨時(中間過程)的,什麼數據是最終結果……利用 Array.prototype 中的相關方法,或者諸如 lodash 之類的工具,很容易就處理出來了。

相關文章
相關標籤/搜索