通過一個場景實例 了解前端處理大數(shù)據(jù)的無限可能

隨著前端的飛速發(fā)展,在瀏覽器端完成復(fù)雜的計算,支配并處理大量數(shù)據(jù)已經(jīng)屢見不鮮。那么,如何在最小化內(nèi)存消耗的前提下,高效優(yōu)雅地完成復(fù)雜場景的處理,越來越考驗開發(fā)者功力,也直接決定了程序的性能。

本文展現(xiàn)了一個完全在控制臺就能模擬體驗的實例,通過一步步優(yōu)化,實現(xiàn)了生產(chǎn)并操控1000000(百萬級別)個對象的場景。

導讀:這篇文章涉及到 javascript 中 數(shù)組各種操作、原型原型鏈、ES6、classes 繼承、設(shè)計模式、控制臺分析 等內(nèi)容。要求閱讀者具有 js 面向?qū)ο笤鷮嵉幕A(chǔ)知識。如果你是初級前端開發(fā)者,很容易被較為復(fù)雜的邏輯繞的云里霧里,“從入門到放棄”,不過建議先收藏。如果你是“老司機”,本文提供的解決思路希望對你有所啟發(fā),拋磚引玉。

場景和初級感知

具體來說,我們需要一個構(gòu)造函數(shù),或者說類似 factory 模式,實例化1000000個以上對象實例。

先來感知一下具體實現(xiàn):

Step1

打開你的瀏覽器控制臺,仔細觀察并復(fù)制粘貼以下代碼,觸發(fā)執(zhí)行。

a = new Array(1e6).fill(0);

我們創(chuàng)建了一個長度為1000000的數(shù)組,數(shù)組的每一項元素都為0。

Step2

在數(shù)組 a 的基礎(chǔ)上,再生產(chǎn)一個長度為1000000的數(shù)組 b,數(shù)組的每一項元素都是一個普通 javascript object,擁有 id 屬性,并且其 id 屬性值為其在元素中的 index 值;

b = a.map((val, ix) => ({id: ix}))

Step3

接下來,在 b 的基礎(chǔ)上,再生產(chǎn)一個長度為1000000的數(shù)組 c ,類似于
b,同時我們增加一些其它屬性,使得數(shù)組元素對象更加復(fù)雜一些:

c = a.map((val, ix) => ({id: ix, shape: 'square', size: 10.5, color: 'green'}))

語義上,我們可以更直觀的理解:c 就是包含了1000000個元素的數(shù)組,每一項都是一個綠色的、size 為10.5的小方塊。

如果你按照指示做了下來,控制臺上會有以下內(nèi)容:

控制臺截圖

深層探究

你也許會想,這么大的數(shù)據(jù)量,內(nèi)存占用會是什么樣的情況呢?

好,我來帶你看看,點擊控制臺 Profiles,選擇 Take Shapshot。在Window->Window 目錄下,根據(jù)內(nèi)存進行篩選,你會得到:

內(nèi)存占用情況

很明顯,我們看到:

  • a數(shù)組:8MB;
  • b數(shù)組:40MB;
  • c數(shù)組:64MB

也許在實際場景中,除了1000000個綠色的、size為10.5的小方塊,我們還需要很多不同顏色,不同 size 的形狀。之前,這樣“變態(tài)”的需求常見于游戲應(yīng)用中。但是現(xiàn)在,復(fù)雜項目中類似場景,也許距離你并不遙遠。

ES6 Classes處理需求

簡單“熱身”之后,我們了解了實際需求。接下來,我們考察一下 ES6 Classes 處理這個問題的情況。請重新刷新瀏覽器 tab,復(fù)制執(zhí)行以下代碼。

class Shape {
    constructor (id, shape = 'square', size = 10.5, color = 'green') {
        this.x = x; //  坐標x軸
        this.y = y; //  坐標y軸
        Object.assign(this, {id, shape, size, color})
    }
}

a = new Array(1e6).fill(0);
b = a.map((val, ix) => new Shape(ix));

我們使用了ES6 Classes,并擴充了每個形狀的坐標信息。
此時,再來看一下內(nèi)存占用情況:

內(nèi)存占用截圖

很明顯,此時 b 數(shù)組由1000000個形狀組成,占據(jù)內(nèi)存:80MB,超過了先前數(shù)組的內(nèi)存消耗。也許這并不出乎意料,此時的b數(shù)組畢竟又多了兩個屬性。

優(yōu)化設(shè)計:Two-Headed Classes

我們先來分析一下上面的實現(xiàn),熟悉原型鏈、原型概念的同學也許會明白,之前的方案產(chǎn)生的實例,順著原型鏈上溯,具有三層原型屬性:

第一層屬性:[id, shape, size, color, x, y]; 這一層屬性的 hasOwnproperty 為 true; 屬性存在于實例本身。

第二層:[Shape]; 順著原型鏈上溯,這一層 instance.proto === Constructor.prototype; ( proto 左右兩邊 __ 被編輯器吃掉了,請見諒,下同)

第三層:[Object]; 這一層: instance.proto.proto === Object.prototype; 如果在向上追溯,就為 null 了。

這樣的情況下,實際業(yè)務(wù)數(shù)據(jù)層只有一層,即為第一層。

但是,請仔細思考,如果有大量的不同顏色,不同size,不同形狀的情況下。單一數(shù)據(jù)層,是難以滿足我們需求的。
我們需要,再添加一層數(shù)據(jù)層,構(gòu)成所謂的 Two-Headed Classes!同時,還需要對于默認的屬性,實現(xiàn)共享,以節(jié)省內(nèi)存的占用。

什么什么?沒聽明白,那就請看具體操作吧。

如何實現(xiàn)?

我們可以使用 Object.create 方法,這樣使得生產(chǎn)得到的實例的
proto 指向 b 數(shù)組的元素,然后在最頂層設(shè)計一個 id 屬性。

也許這樣說過于晦澀,那就直接參考代碼吧,請注意,這是本篇文章最難以理解的地方,請務(wù)必仔細揣摩:

two = Object.create(b[0]); 
// two.__proto__ === b[0]
two.id = 1;

還記得 b 數(shù)組是什么嘛?參考上文,它由

b = a.map((val, ix) => new Shape(ix));

得到。

這樣子的話,對于每一個實例,我們有如下關(guān)系:

第一層:[id]; 這一層實例的 hasOwnproperty 為 true;

第二層:[id, shape, size, color, x, y]; 這一層 instance.proto === Constructor.prototype;

第三層:[Shape];

第四層:[Object]; 這一層的再頂層,就為null了。

我們將 Shape 的一個實例作為一個新的 object 的原型,并復(fù)寫了 id 屬性,原有的 id 屬性將作為默認 id。

當然,上邊的代碼只是“個案”,我們進行“生產(chǎn)化”:

proto = new Shape(0);
function newTwoHeaded (ix) {
    const obj = Object.create(proto);
    obj.id = ix;
    return obj
}
c = a.map((val, ix) => newTwoHeaded(ix));

這么做多加入了一個數(shù)據(jù)層,那么有什么“收獲”呢?我們來看一下b和c的內(nèi)存占用情況吧:

內(nèi)存占用截圖

這表明:我們從80MB的b,優(yōu)化得到了64MB的c!
**原因當然就在于雖然多加了一層原型結(jié)構(gòu),但是第二層變成了“共享”。 **

當然,如果到這里你還沒有暈的話,可能要問:那第二層諸如 shape, size, color 這些屬性變成共享的之后,存在互相干擾怎么破解呢?

好問題,我先不解答,先給大家看一下最后的final product:

class ShapeMaker {
    constructor () {
        Object.assign(this, ShapeMaker.defaults())
    }
    static defaults () {
        return {
            id: null,
            x: 0,
            y: 0,
            shape: 'square',
            size: 0.5,
            color: 'red',
            strokeColor: 'yellow',
            hidden: false,
            label: null,
            labelOffset: [0, 0],
            labelFont: '10px sans-serif',
            labelColor: 'black'
        }
    }
    newShape (id, x, y) {
        const obj = Object.create(this);
        return Object.assign(obj, {id, x, y})
    }
    setDefault (name, value) {
        this[name] = value;
    }
    getDefault (name) {
        return this[name]
    }
}

在實例化的時候,我們便可以這樣使用:

shapeProto = new ShapreMaker();
d = a.map((val, ix) => shapeProto.newShape(ix, ix/10, -ix/10))

就像上面所說的,初始化實例時,我們初始化了 id, x, y 這么三個參數(shù)。作為該實例本身的數(shù)據(jù)層。這個實例的原型上,也有類似的參數(shù),來保證默認值。這些原型上的屬性,對于實例數(shù)組中的每個實例,都是共享的。

為了更好的對比,如果設(shè)計是這樣子:

function fatShape (id, x, y) {
    const a = new shapeMaker();
    return Object.assign(a, {id, x, y})
}
e = a.map((val, ix) => fatShape(ix, ix/10, -ix/10))

那么所有屬性無法共享,而是各自拷貝了一份。在內(nèi)存的占用上,將是我們給出方案的三倍之多!

內(nèi)存占用截圖

阿喀琉斯之踵

阿喀琉斯,是凡人珀琉斯和美貌仙女忒提斯的寶貝兒子。忒提斯為了讓兒子煉成“金鐘罩”,在他剛出生時就將其倒提著浸進冥河,遺憾的是,乖兒被母親捏住的腳后跟卻不慎露在水外,全身留下了惟一一處“死穴”。后來,阿喀琉斯被帕里斯一箭射中了腳踝而死去。
后人常以“阿喀琉斯之踵”譬喻這樣一個道理:即使是再強大的英雄,他也有致命的死穴或軟肋。

就像我們剛才提的到解決方案一樣,也有一些“不足”。問題其實在之前我也已經(jīng)拋出:“第二層諸如:shape, size, color 這些屬性變成共享的之后,存在互相干擾怎么破解呢?”

這個問題的答案其實也隱藏在上面的代碼中,很簡單,就是我們在實例的自身屬性上,進行復(fù)寫,而避免更改原型上的屬性造成污染。

如果你看的云里霧里,不要緊,馬上看一下我下面的代碼說明:

d.every((item) => item.shape === 'square') // true

打印為 true,是因為 d 數(shù)組中的每個實例的 shape 屬性,都在原型上,且初始值都為'square';

現(xiàn)在我們調(diào)用 setDefault 方法,實現(xiàn)對默認 shape 的改寫。

shapeProto.setDefault('shape', 'circle');
d.every((item) => item.shape === 'square'); // false

因為此時所有實例的 shape 都在原型上,并共享這個原型。更改之后,我們有:

d.every((item) => item.shape === 'circle'); // true

但是,我只想把第一個實例的 shape 設(shè)置為 triangle,其他的不變,該怎么辦呢?只需要在第一個實例上,增加一個 shape 屬性,進行重寫:

d[0].shape = 'triangle';
d.every((item) => item.shape === 'circle'); // false

好吧,嘗試完畢之后,我們在變回來。

d[0].shape = 'circle';

這時候,自然有:

d.every((item) => item.shape === 'circle'); // true

同時,再折騰一下:

d[0].shape = 'triangle';
d.every((item) => item.shape === 'triangle'); // false

相信下面的也不難理解了:

shapeProto.setDefault('shape', 'triangle');
d.every((item) => item.shape === 'triangle'); // true

這種模式其實比單純使用ES6 Classes要靈活的多,同時也節(jié)省了內(nèi)存。所有的靜態(tài)屬性都是共享的,但是共享的靜態(tài)屬性又都是可變的,可復(fù)寫的。

總結(jié)

這篇文章,我們在開頭部分了解到了在大量數(shù)據(jù)的情況下,內(nèi)存的占用是如何一步一步變的沉重。同時,我們提供了一種,在傳統(tǒng)的
Classes 之上增加一個數(shù)據(jù)層的方法,有效地解決了這個問題。解決方案充分利用了 Object.create 等手段。

當然,理解這些內(nèi)容并不簡單,需要讀者有比較扎實的 javascript 基礎(chǔ)。在您閱讀過程當中,有任何問題,歡迎與我討論。

內(nèi)容借鑒了Owen Densmore最新文章:Two Headed ES6 Classes!,喜歡看英文原版的同學可以直接戳鏈接。中文翻譯版并非直譯,進行了較大幅度的講解和增刪。

Happy Coding!

PS:
作者Github倉庫知乎問答鏈接
歡迎各種形式交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,525評論 19 139
  • 原型鏈是一種機制,指的是 JavaScript 每個對象都有一個內(nèi)置的 __proto__ 屬性指向創(chuàng)建它的構(gòu)造函...
    劼哥stone閱讀 4,567評論 15 80
  • 掉頭發(fā)在秋冬季節(jié)比較多 掉頭發(fā)人人都有,不過有的人掉得多,有的人掉得少。而有些人還會一片一片地掉,最后成了禿子。這...
    吳蕓蕓閱讀 734評論 0 0
  • 請同時放送:【再見 張震岳】 陳太太 我認識陳太太20多年了,也算是我的舊時,她脾氣不好,總是對我大吼大叫,但我們...
    陳杦閱讀 576評論 0 4
  • 我一直認為我自己是一個性格說好不好,說壞也算不上壞的,總之就是沒什么特點,但是最近我有很多感觸,原來我其實我還是懂...
    宅豆芽閱讀 270評論 0 0

友情鏈接更多精彩內(nèi)容