javascript 大數據處理方法

隨着前端的飛速發展,在瀏覽器端完成複雜的計算,支配並處理大量數據已經家常便飯。那麼,如何在最小化內存消耗的前提下,高效優雅地完成複雜場景的處理,愈來愈考驗開發者功力,也直接決定了程序的性能。javascript

本文展示了一個徹底在控制檯就能模擬體驗的實例,經過一步步優化,實現了生產並操控多個1000000(百萬級別)對象的場景。css

導讀:這篇文章涉及到 javascript 中 數組各類操做、原型原型鏈、ES六、classes 繼承、設計模式、控制檯分析 等內容。前端

要求閱讀者具備 js 面向對象紮實的基礎知識。若是你是初級前端開發者,很容易被較爲複雜的邏輯繞的雲裏霧裏,「從入門到放棄」,不過建議先收藏。若是你是「老司機」,本文提供的解決思路但願對你有所啓發,拋磚引玉。java

 

場景和初級感知

具體來講,咱們須要一個構造函數,或者說相似 factory 模式,實例化1000000個以上對象實例。jquery

先來感知一下具體實現:設計模式

Step1

打開你的瀏覽器控制檯,仔細觀察並複製粘貼如下代碼,觸發執行。數組

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

咱們建立了一個長度爲1000000的數組,數組的每一項元素都爲0。瀏覽器

Step2

在數組 a 的基礎上,再生產一個長度爲1000000的數組 b,數組的每一項元素都是一個普通 javascript object,擁有 id 屬性,而且其 id 屬性值爲其在元素中的 index 值;微信

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

Step3

接下來,在 b 的基礎上,再生產一個長度爲1000000的數組 c ,相似於 b,同時咱們增長一些其它屬性,使得數組元素對象更加複雜一些:app

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

語義上,咱們能夠更直觀的理解:c 就是包含了1000000個元素的數組,每一項都是一個綠色的、size 爲10.5的小方塊。

若是你按照指示作了下來,控制檯上會有如下內容:

 

 

 

 

深層探究

你也許會想,這麼大的數據量,內存佔用會是什麼樣的狀況呢?

好,我來帶你看看,點擊控制檯 Profiles,選擇 Take Shapshot。在Window->Window 目錄下,根據內存進行篩選,你會獲得:

 

很明顯,咱們看到:

  • a數組:8MB;
  • b數組:40MB;
  • c數組:64MB

也許在實際場景中,除了1000000個綠色的、size爲10.5的小方塊,咱們還須要不少不一樣顏色,不一樣 size 的形狀。以前,這樣「變態」的需求常見於遊戲應用中。可是如今,複雜項目中相似場景,也許距離你並不遙遠。

ES6 Classes處理需求

簡單「熱身」以後,咱們瞭解了實際需求。接下來,咱們考察一下 ES6 Classes 處理這個問題的狀況。請從新刷新瀏覽器 tab,複製執行如下代碼。

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,並擴充了每一個形狀的座標信息。 此時,再來看一下內存佔用狀況:

 

很明顯,此時 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 屬性。

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

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

還記得 b 數組是什麼嘛?參考上文,它由

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

獲得。

這樣子的話,對於每個實例,咱們有以下關係:

第一層:[id]; 這一層實例的 hasOwnproperty 爲 true;
第二層:[id, shape, size, color, x, y]; 這一層 instance.proto === Constructor.prototype;
第三層:[Shape];
第四層:[Object]; 這一層的再頂層,就爲null了。

咱們將 Shape 的一個實例做爲一個新的 object 的原型,並複寫了 id 屬性,原有的 id 屬性將做爲默認 id。

固然,上邊的代碼只是「個案」,咱們進行「生產化」:

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

這麼作多加入了一個數據層,那麼有什麼「收穫」呢?咱們來看一下b和c的內存佔用狀況吧:

這代表:咱們從80MB的b,優化獲得了64MB的c! 緣由固然就在於雖然多加了一層原型結構,可是第二層變成了「共享」。

固然,若是到這裏你尚未暈的話,可能要問:那第二層諸如 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 這麼三個參數。做爲該實例自己的數據層。這個實例的原型上,也有相似的參數,來保證默認值。這些原型上的屬性,對於實例數組中的每一個實例,都是共享的。

爲了更好的對比,若是設計是這樣子:

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

那麼全部屬性沒法共享,而是各自拷貝了一份。在內存的佔用上,將是咱們給出方案的三倍之多!

 

阿喀琉斯之踵

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

就像咱們剛纔提的到解決方案同樣,也有一些「不足」。問題其實在以前我也已經拋出:「第二層諸如:shape, size, color 這些屬性變成共享的以後,存在互相干擾怎麼破解呢?」

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

若是你看的雲裏霧裏,沒關係,立刻看一下我下面的代碼說明:

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

打印爲 true,是由於 d 數組中的每一個實例的 shape 屬性,都在原型上,且初始值都爲'square';

如今咱們調用 setDefault 方法,實現對默認 shape 的改寫。

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

由於此時全部實例的 shape 都在原型上,並共享這個原型。更改以後,咱們有:

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

可是,我只想把第一個實例的 shape 設置爲 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要靈活的多,同時也節省了內存。全部的靜態屬性都是共享的,可是共享的靜態屬性又都是可變的,可複寫的。

 

總結

這篇文章,咱們在開頭部分了解到了在大量數據的狀況下,內存的佔用是如何一步一步變的沉重。同時,咱們提供了一種,在傳統的 Classes 之上增長一個數據層的方法,有效地解決了這個問題。解決方案充分利用了 Object.create 等手段。

固然,理解這些內容並不簡單,須要讀者有比較紮實的 javascript 基礎。在您閱讀過程中,有任何問題,歡迎與我討論。

 

---恢復內容結束---

<head>
<link rel="stylesheet" href="http://apps.bdimg.com/libs/animate.css/3.1.0/animate.min.css">
<style type="text/css">
#pay_pic{
overflow: hidden;
width: 200px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="pay_area" style="padding: 10px;border-radius: 5px;background-color:#EDDCBE;text-align:center;font-size: 15px;color: #272822;cursor:pointer;margin: 10px;">
<div id="pay_pic" align="center" class="">
<img src="http://images.cnblogs.com/cnblogs_com/hngdlxy143/1219034/o_in.png" width="200px">
</div>
<div align="center">微信掃一掃打賞支持</div>
<div align="center">Q羣交流<span style="border-bottom:1px solid #B82525;font-weight: bold;">651176910</span></div>
<div align="center">共<span style="font-weight: bold; color: #B82525">69</span>人支持!!!</div>
</div>

<script type="text/javascript" src=" http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
alert();
currentDiggType = 0;
$("#pay_pic").hover(
function () {
$(this).addClass("animated swing");
},
function () {
$(this).removeClass("animated swing");
}
);
</script>
</body>

 

隨着前端的飛速發展,在瀏覽器端完成複雜的計算,支配並處理大量數據已經家常便飯。那麼,如何在最小化內存消耗的前提下,高效優雅地完成複雜場景的處理,愈來愈考驗開發者功力,也直接決定了程序的性能。

本文展示了一個徹底在控制檯就能模擬體驗的實例,經過一步步優化,實現了生產並操控多個1000000(百萬級別)對象的場景。

導讀:這篇文章涉及到 javascript 中 數組各類操做、原型原型鏈、ES六、classes 繼承、設計模式、控制檯分析 等內容。

要求閱讀者具備 js 面向對象紮實的基礎知識。若是你是初級前端開發者,很容易被較爲複雜的邏輯繞的雲裏霧裏,「從入門到放棄」,不過建議先收藏。若是你是「老司機」,本文提供的解決思路但願對你有所啓發,拋磚引玉。

 

場景和初級感知

具體來講,咱們須要一個構造函數,或者說相似 factory 模式,實例化1000000個以上對象實例。

先來感知一下具體實現:

Step1

打開你的瀏覽器控制檯,仔細觀察並複製粘貼如下代碼,觸發執行。

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

咱們建立了一個長度爲1000000的數組,數組的每一項元素都爲0。

Step2

在數組 a 的基礎上,再生產一個長度爲1000000的數組 b,數組的每一項元素都是一個普通 javascript object,擁有 id 屬性,而且其 id 屬性值爲其在元素中的 index 值;

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

Step3

接下來,在 b 的基礎上,再生產一個長度爲1000000的數組 c ,相似於 b,同時咱們增長一些其它屬性,使得數組元素對象更加複雜一些:

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

語義上,咱們能夠更直觀的理解:c 就是包含了1000000個元素的數組,每一項都是一個綠色的、size 爲10.5的小方塊。

若是你按照指示作了下來,控制檯上會有如下內容:

 

 

 

 

深層探究

你也許會想,這麼大的數據量,內存佔用會是什麼樣的狀況呢?

好,我來帶你看看,點擊控制檯 Profiles,選擇 Take Shapshot。在Window->Window 目錄下,根據內存進行篩選,你會獲得:

 

很明顯,咱們看到:

  • a數組:8MB;
  • b數組:40MB;
  • c數組:64MB

也許在實際場景中,除了1000000個綠色的、size爲10.5的小方塊,咱們還須要不少不一樣顏色,不一樣 size 的形狀。以前,這樣「變態」的需求常見於遊戲應用中。可是如今,複雜項目中相似場景,也許距離你並不遙遠。

ES6 Classes處理需求

簡單「熱身」以後,咱們瞭解了實際需求。接下來,咱們考察一下 ES6 Classes 處理這個問題的狀況。請從新刷新瀏覽器 tab,複製執行如下代碼。

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,並擴充了每一個形狀的座標信息。 此時,再來看一下內存佔用狀況:

 

很明顯,此時 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 屬性。

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

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

還記得 b 數組是什麼嘛?參考上文,它由

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

獲得。

這樣子的話,對於每個實例,咱們有以下關係:

第一層:[id]; 這一層實例的 hasOwnproperty 爲 true;
第二層:[id, shape, size, color, x, y]; 這一層 instance.proto === Constructor.prototype;
第三層:[Shape];
第四層:[Object]; 這一層的再頂層,就爲null了。

咱們將 Shape 的一個實例做爲一個新的 object 的原型,並複寫了 id 屬性,原有的 id 屬性將做爲默認 id。

固然,上邊的代碼只是「個案」,咱們進行「生產化」:

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

這麼作多加入了一個數據層,那麼有什麼「收穫」呢?咱們來看一下b和c的內存佔用狀況吧:

這代表:咱們從80MB的b,優化獲得了64MB的c! 緣由固然就在於雖然多加了一層原型結構,可是第二層變成了「共享」。

固然,若是到這裏你尚未暈的話,可能要問:那第二層諸如 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 這麼三個參數。做爲該實例自己的數據層。這個實例的原型上,也有相似的參數,來保證默認值。這些原型上的屬性,對於實例數組中的每一個實例,都是共享的。

爲了更好的對比,若是設計是這樣子:

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

那麼全部屬性沒法共享,而是各自拷貝了一份。在內存的佔用上,將是咱們給出方案的三倍之多!

 

阿喀琉斯之踵

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

就像咱們剛纔提的到解決方案同樣,也有一些「不足」。問題其實在以前我也已經拋出:「第二層諸如:shape, size, color 這些屬性變成共享的以後,存在互相干擾怎麼破解呢?」

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

若是你看的雲裏霧裏,沒關係,立刻看一下我下面的代碼說明:

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

打印爲 true,是由於 d 數組中的每一個實例的 shape 屬性,都在原型上,且初始值都爲'square';

如今咱們調用 setDefault 方法,實現對默認 shape 的改寫。

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

由於此時全部實例的 shape 都在原型上,並共享這個原型。更改以後,咱們有:

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

可是,我只想把第一個實例的 shape 設置爲 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要靈活的多,同時也節省了內存。全部的靜態屬性都是共享的,可是共享的靜態屬性又都是可變的,可複寫的。

 

總結

這篇文章,咱們在開頭部分了解到了在大量數據的狀況下,內存的佔用是如何一步一步變的沉重。同時,咱們提供了一種,在傳統的 Classes 之上增長一個數據層的方法,有效地解決了這個問題。解決方案充分利用了 Object.create 等手段。

固然,理解這些內容並不簡單,須要讀者有比較紮實的 javascript 基礎。在您閱讀過程中,有任何問題,歡迎與我討論。

相關文章
相關標籤/搜索