前端處理大數據的無限可能
前言
今日早讀文章由 百度 @Lucas 授權分享。
正文從這開始~
隨著前端的飛速發展,在瀏覽器端完成複雜的計算,支配並處理大量數據已經屢見不鮮。那麼,如何在最小化內存消耗的前提下,高效優雅地完成複雜場景的處理,越來越考驗開發者功力,也直接決定了程序的性能。
本文展現了一個完全在控制台就能模擬體驗的實例,通過一步步優化,實現了生產並操控多個100000(十萬級別)對象的場景。
導讀:這篇文章涉及到 javascript 中 數組各種操作、原型原型鏈、ES6、classes 繼承、設計模式、控制台分析 等內容。
要求閱讀者具有 js 面向對象紮實的基礎知識。如果你是初級前端開發者,很容易被較為複雜的邏輯繞的雲里霧裡,「從入門到放棄」,不過建議先收藏。如果你是「老司機」,本文提供的解決思路希望對你有所啟發,拋磚引玉。
場景和初級感知
具體來說,我們需要一個構造函數,或者說類似 factory 模式,實例化100000個以上對象實例。
先來感知一下具體實現:
Step1
打開你的瀏覽器控制台,仔細觀察並複製粘貼以下代碼,觸發執行。
a=newArray(1e6).fill();
我們創建了一個長度為100000的數組,數組的每一項元素都為0。
Step2
在數組 a 的基礎上,再生產一個長度為100000的數組 b,數組的每一項元素都是一個普通 javascript object,擁有 id 屬性,並且其 id 屬性值為其在元素中的 index 值;
b=a.map((val,ix)=>({id:ix}))
Step3
接下來,在 b 的基礎上,再生產一個長度為100000的數組 c ,類似於 b,同時我們增加一些其它屬性,使得數組元素對象更加複雜一些:
c=a.map((val,ix)=>({id:ix,shape: square ,size:10.5,color: green }))
語義上,我們可以更直觀的理解:c 就是包含了100000個元素的數組,每一項都是一個綠色的、size 為10.5的小方塊。
如果你按照指示做了下來,控制台上會有以下內容:
深層探究
你也許會想,這麼大的數據量,內存佔用會是什麼樣的情況呢?
好,我來帶你看看,點擊控制台 Profiles,選擇 Take Shapshot。在Window->Window 目錄下,根據內存進行篩選,你會得到:
很明顯,我們看到:
a數組:8MB;
b數組:40MB;
c數組:64MB
也許在實際場景中,除了100000個綠色的、size為10.5的小方塊,我們還需要很多不同顏色,不同 size 的形狀。之前,這樣「變態」的需求常見於遊戲應用中。但是現在,複雜項目中類似場景,也許距離你並不遙遠。
ES6 Classes處理需求
簡單「熱身」之後,我們了解了實際需求。接下來,我們考察一下 ES6 Classes 處理這個問題的情況。請重新刷新瀏覽器 tab,複製執行以下代碼。
classShape{
classShape{constructor(id,shape= square ,size=10.5,{id,shape,size,color})}}a=newArray(1e6).fill();b=a.map((val,ix)=>newShape(ix));(,= square ,=10.5,= green ){
this.=;// 坐標x軸
this.=;// 坐標y軸
.assign(this,{,,,})
}
}
=newArray(1e6).fill();
=.map((,)=>newShape());
我們使用了ES6 Classes,並擴充了每個形狀的坐標信息。 此時,再來看一下內存佔用情況:
很明顯,此時 b 數組由100000個形狀組成,佔據內存:80MB,超過了先前數組的內存消耗。也許這並不出乎意料,此時的b數組畢竟又多了兩個屬性。
優化設計:Two-Headed Classes
我們先來分析一下上面的實現,熟悉原型鏈、原型概念的同學也許會明白,之前的方案產生的實例,順著原型鏈上溯,具有三層原型屬性:
第一層:[id, shape, size, color, x, y]; 這一層屬性的 hasOwnproperty 為 true; 屬性存在於實例本身。
第二層:[Shape]; 順著原型鏈上溯,這一層 instance.proto === Constructor.prototype; ( proto 左右兩邊 __ 被編輯器吃掉了,請見諒,下同)
第三層:[Object]; 這一層: instance.proto.__proto__ === Object.prototype; 如果在向上追溯,就為 null 了。
這樣的情況下,實際業務數據層只有一層,即為第一層。
但是,請仔細思考,如果有大量的不同顏色,不同size,不同形狀的情況下。單一數據層,是難以滿足我們需求的。 我們需要,再添加一層數據層,構成所謂的 Two-Headed Classes!同時,還需要對於默認的屬性,實現共享,以節省內存的佔用。
什麼什麼?沒聽明白,那就請看具體操作吧。
如何實現?
我們可以使用 Object.create 方法,這樣使得生產得到的實例的 proto 指向 b 數組的元素,然後在最頂層設計一個 id 屬性。
也許這樣說過於晦澀,那就直接參考代碼吧,請注意,這是本篇文章最難以理解的地方,請務必仔細揣摩:
還記得 b 數組是什麼嘛?參考上文,它由
b=a.map((val,ix)=>newShape(ix));
得到。
這樣子的話,對於每一個實例,我們有如下關係:
第一層:[id]; 這一層實例的 hasOwnproperty 為 true;
第二層:[id, shape, size, color, x, y]; 這一層 instance.proto === Constructor.prototype;
第三層:[Shape];
第四層:[Object]; 這一層的再頂層,就為null了。
我們將 Shape 的一個實例作為一個新的 object 的原型,並複寫了 id 屬性,原有的 id 屬性將作為默認 id。
當然,上邊的代碼只是「個案」,我們進行「生產化」:
proto=newShape();
functionnewTwoHeaded(ix){
constobj=Object.create(proto);
obj.id=ix;
returnobj
}
c=a.map((val,ix)=>newTwoHeaded(ix));
這麼做多加入了一個數據層,那麼有什麼「收穫」呢?我們來看一下b和c的內存佔用情況吧:
這表明:我們從80MB的b,優化得到了64MB的c! 原因當然就在於雖然多加了一層原型結構,但是第二層變成了「共享」。
當然,如果到這裡你還沒有暈的話,可能要問:那第二層諸如 shape, size, color 這些屬性變成共享的之後,存在互相干擾怎麼破解呢?
好問題,我先不解答,先給大家看一下最後的 final product:
classShapeMaker{
constructor(){
Object.assign(this,ShapeMaker.defaults())
}
staticdefaults(){
return{
id:null,
x:,
y:,
shape: square ,
size:0.5,
color: red ,
strokeColor: yellow ,
hidden:false,
label:null,
labelOffset:[,],
labelFont: 10px sans-serif ,
labelColor: black
}
}
newShape(id,x,y){
constobj=Object.create(this);
returnObject.assign(obj,{id,x,y})
}
setDefault(name,value){
this[name]=value;
}
getDefault(name){
returnthis[name]
}
}
在實例化的時候,我們便可以這樣使用:
shapeProto=newShapreMaker();d=a.map((val,ix)=>shapeProto.newShape(ix=newShapreMaker();
=.map((,)=>.newShape(,/10,-/10))
就像上面所說的,初始化實例時,我們初始化了 id, x, y 這麼三個參數。作為該實例本身的數據層。這個實例的原型上,也有類似的參數,來保證默認值。這些原型上的屬性,對於實例數組中的每個實例,都是共享的。
為了更好的對比,如果設計是這樣子:
functionfunctionfatShape(id,x,y){consta=newshapeMaker();returnObject.assign(a,{id,x,y})}e=a.map((val,ix)=>fatShape(ix(,,){
const=newshapeMaker();
return.assign(,{,,})
}
=.map((,)=>fatShape(,/10,-/10))
那麼所有屬性無法共享,而是各自拷貝了一份。在內存的佔用上,將是我們給出方案的三倍之多!
阿喀琉斯之踵
阿喀琉斯,是凡人珀琉斯和美貌仙女忒提斯的寶貝兒子。忒提斯為了讓兒子煉成「金鐘罩」,在他剛出生時就將其倒提著浸進冥河,遺憾的是,乖兒被母親捏住的腳後跟卻不慎露在水外,全身留下了惟一一處「死穴」。後來,阿喀琉斯被帕里斯一箭射中了腳踝而死去。 後人常以「阿喀琉斯之踵」譬喻這樣一個道理:即使是再強大的英雄,他也有致命的死穴或軟肋。
就像我們剛才提的到解決方案一樣,也有一些「不足」。問題其實在之前我也已經拋出:「第二層諸如:shape, size, color 這些屬性變成共享的之後,存在互相干擾怎麼破解呢?」
這個問題的答案其實也隱藏在上面的代碼中,很簡單,就是我們在實例的自身屬性上,進行複寫,而避免更改原型上的屬性造成污染。
如果你看的雲里霧裡,不要緊,馬上看一下我下面的代碼說明:
列印為 true,是因為 d 數組中的每個實例的 shape 屬性,都在原型上,且初始值都為 square ;
現在我們調用 setDefault 方法,實現對默認 shape 的改寫。
因為此時所有實例的 shape 都在原型上,並共享這個原型。更改之後,我們有:
但是,我只想把第一個實例的 shape 設置為 triangle,其他的不變,該怎麼辦呢?只需要在第一個實例上,增加一個 shape 屬性,進行重寫:
好吧,嘗試完畢之後,我們在變回來。
d[].shape= circle ;
這時候,自然有:
同時,再折騰一下:
相信下面的也不難理解了:
這種模式其實比單純使用ES6 Classes要靈活的多,同時也節省了內存。所有的靜態屬性都是共享的,但是共享的靜態屬性又都是可變的,可複寫的。
總結
這篇文章,我們在開頭部分了解到了在大量數據的情況下,內存的佔用是如何一步一步變的沉重。同時,我們提供了一種,在傳統的 Classes 之上增加一個數據層的方法,有效地解決了這個問題。解決方案充分利用了 Object.create 等手段。
當然,理解這些內容並不簡單,需要讀者有比較紮實的 javascript 基礎。在您閱讀過程當中,有任何問題,歡迎與我討論。
內容借鑒了Owen Densmore最新文章:Two Headed ES6 Classes!,喜歡看英文原版的同學可以直接戳鏈接。中文翻譯版並非直譯,進行了較大幅度的講解和增刪。
關於本文
作者:@Lucas
※redux-react實踐總結
※【第994期】字型大小與行高
※總是一知半解的Event Loop
※美團金融大前端團隊招各級別前端工程師
※手把手教你用ngrx管理Angular狀態(下)
TAG:前端早讀課 |
※數據產品:無處不在的數據決策
※銷售易發布智能分析雲 告別傳統BI 讓數據擁有無限可能
※大數據的外部特性背後,是無處安放的個人隱私
※大數據分析中不可避免的數據價值定位和數據前瞻定位
※處理不平衡數據的技巧總結!
※大數據如作惡,我們將毫無隱私!
※大數據之下,消費者並不是全無勝算
※數據可視化工具:大數據賦能人工智慧大有可為
※大數據殺熟最終會得不償失
※大數據管理不應是「法外之地」
※「大數據殺熟」?商家對數據的使用可能遠超出你的想像
※沒有數據泄漏,就沒大數據產業?
※大數據殺熟的背後,是人性的貪慾
※能不能好好的做一門生意?——關於大數據殺熟
※AI前置帶來監控數據存儲和處理模式的變化
※推斷時代的數據流動性:概率計算帶來了太多的希望,但這一切可能被數據的零和博弈所抑制
※如何有效應對大數據技術的倫理挑戰?
※大數據匹配婚戀可期待但不依賴
※數據預處理——數據清洗
※大數據競爭中的法律局限性