前端數據扁平化與持久化

(PS: 時間就像海綿裏的水,擠到無法擠,只能擠擠睡眠時間了~ 知識點仍是須要整理的,付出總會有收穫,tired but fulfilled~)javascript

前言

最近業務開發,從零搭建網頁生成器,支持網頁的可視化配置。爲了知足這種需求,須要將各類頁面抽象成相似地模塊,再將每一個模塊抽象成各個可配置的組件,有些組件還包含一些小部件。這樣一來,頁面配置的JSON數據就會深層級地嵌套,那麼修改一個小組件的配置,要怎樣來更新頁面樹的數據?用id一層一層遍歷?這樣作法固然是不推薦的,不只性能差,代碼寫起來也麻煩。所以,就考慮可否像數據庫同樣,把數據範式化,將嵌套的數據展開,每條數據對應一個id,經過id直接操做。Normalizr 就幫你作了這樣一件事情。html

另外考慮到頁面編輯,就須要支持 撤銷重作的功能,那麼要怎樣來保存每一步的數據?頁面編輯的數據互相關聯,對象的可變性會帶來很大的隱患。雖然JS中的const(es6)Object.freeze(es5) 能夠防止數據被修改,但它們都是shallow處理,遇到嵌套多和深的結構就須要遞歸處理,而遞歸又存在性能上的問題。這時,用過React的童鞋就知道了,React藉助 Immutable 來減小DOM diff的比對,它就可以很好地解決上面這兩個問題。Immutable 實現的原理是 Persistent Data Structure(持久化數據結構),也就是使用舊數據建立新數據時,要保證舊數據同時可用且不變。前端

那麼爲何在JS中,諸如對象這樣的數據類型是可變的呢?咱們先來了解一下JS的數據類型。java

JS數據類型

JS的數據類型包括基本類型和引用類型。基本類型包括String、Number、 Boolean、Null、Undefined,引用類型主要是對象(包括Object、Function、Array、Data等)。基礎類型的值自己沒法被改變,而引用類型,如Object,是能夠被改變的。本文討論的數據不可變,就是指保持對象的狀態不變。來看看下面的例子:react

// 基本類型

var a = 1;
var b = a;
b = 3;
console.log(a); // 1
console.log(b); // 3

// 引用類型
var obj1 = {};
obj1.arr = [2,3,4];
var obj2 = obj1;
obj2.arr.push(5);

console.log(obj1.arr); // [2, 3, 4, 5]
console.log(obj2.arr); // [2, 3, 4, 5]

上面例子中,b的值改變後,a的值不會隨着改變;而obj2.arr被修改後,obj1.arr的值卻跟着變化了。這是由於JS對象中的賦值是「引用賦值」,即在賦值的過程當中,傳遞的是在內存中的引用。這也是JS中對象爲何有深拷貝和淺拷貝的用法,只有深拷貝後,對新對象的修改纔不會改變原來的對象。
淺拷貝只會將對象的各個屬性進行依次複製,並不會進行遞歸複製,而 JavaScript 存儲對象都是存地址的。上面代碼中,只是執行了淺拷貝,結果致使 obj1obj2指向同一塊內存地址。因此修改obj2.arrobj1.arr的值也變了。若是是深拷貝(如Lodash的cloneDeep)則不一樣,它不只將原對象的各個屬性逐個複製出去,並且將原對象各個屬性所包含的對象也依次採用深拷貝的方法遞歸複製到新對象上,也就不會存在上面 obj1obj2 中的 arr 屬性指向同一個內存對象的問題。git

爲了更清晰地理解這個問題,仍是得來了解下javascript變量的存儲方式。es6

數據類型的存儲

程序的運行都須要內存,JS語言把數據分配到內存的棧(stack)和堆(heap)進行各類調用(注:內存中除了棧和堆,還有常量池)。JS這樣分配內存,與它的垃圾回收機制有關,可使程序運行時佔用的內存最小。github

在JS中,每一個方法被執行時,都會創建本身的內存棧,這個方法內定義的變量就會一一被放入這個棧中。等到方法執行結束,它的內存棧也天然地銷燬了。所以,全部在方法中定義的變量都是放在棧內存中的。當咱們在程序中建立一個對象時,這個對象將被保存到運行時數據區中,以便反覆利用(由於對象的建立成本一般較大),這個運行時數據區就是堆內存。堆內存中的對象不會隨方法的結束而銷燬,即便方法結束後,這個對象還可能被另外一個引用變量所引用。只有當一個對象沒有任何引用變量引用它時,系統的垃圾回收機制纔會在覈實的時候回收它。數據庫

總的來講,棧中存儲的是基礎變量以及一些對象的引用變量,基礎變量的值是存儲在棧中,而引用變量存儲在棧中的是指向堆中的對象的地址,這就是修改引用類型總會影響到其餘指向這個地址的引用變量的緣由。堆是運行時動態分配內存的,存取速度較慢,棧的優點是存取速度比堆要快,而且棧內的數據能夠共享,可是棧中數據的大小與生存期必須是肯定的,缺少靈活性。編程

Normalizr與範式化

範式化(Normalization)是數據庫設計中的一系列原理和技術,以減小數據庫中數據冗餘,增進數據的一致性。直觀地描述就是尋找對象之間的關係,經過某種方式將關係之間進行映射,減小數據之間的冗餘,優化增刪改查操做。Normalizr庫自己的解釋就是Normalizes nested JSON according to a schema),一種相似於關係型數據庫的處理方法,經過建表創建數據關係,把深層嵌套的數據展開,更方便靈活的處理和操做數據。

來看個官網的例子,理解一下:

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}

這是一份博客的數據,一篇文章article有一個做者author, 一個標題title, 多條評論,每條評論有一個評論者commenter,每一個commenter又有本身的id和name。這樣若是咱們要獲取深層級的數據,如commenter時,就須要層層遍歷。這時候,若是使用Normalizr,就能夠這樣定義Schema:

import { schema } from 'normalizr';  

const user = new schema.Entity('users');

const comment = new schema.Entity('comments', {
  commenter: user
});

const article = new schema.Entity('articles', {
  author: user,
  comments: [comment]
});

而後調用一下 Normalize,就能夠獲得扁平化後的數據,以下:

{
  "entities": {
    "users": {
      "1": {
        "id": "1",
        "name": "Paul"
      },
      "2": {
        "id": "2",
        "name": "Nicole"
      }
    },
    "comments": {
      "324": {
        "id": "324",
        "commenter": "2"
      }
    },
    "articles": {
      "123": {
        "id": "123",
        "author": "1",
        "title": "My awesome blog post",
        "comments": ["324"]
      }
    }
  },
  "result": "123"
}

這樣每一個做者、每條評論、每篇文章都有對應的id, 咱們就不須要遍歷,能夠直接拿對應的id進行修改。

再來看下咱們在項目中的示例代碼:

分別定義element、section 和 page三張表,並指定它們之間的關係。這樣範式化後,想對某個頁面某個模塊或者某個元素進行增刪查改,就直接拿對應的id,不須要再耗性能去遍歷了。

Immutable與持久化

Facebook工程師Lee Byron花了3年時間打造Immutable,與 React 同期出現。Immutable Data,維基百科上是這樣定義的:

In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure.

簡單來講,Immutable Data 就是一旦建立,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操做都會返回一個新的 Immutable 對象。Immutable 實現的原理是 Persistent Data Structure(持久化數據結構),也就是使用舊數據建立新數據時,要保證舊數據同時可用且不變。Immutable 使用了 Structural Sharing(結構共享),即若是對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享,這樣就避免了深拷貝帶來的性能損耗。

咱們經過圖片來理解一下:

Immutable 內部實現了一套完整的持久化數據結構,有不少易用的數據類型,如Collection、List、Map、Set、Record、Seq(Seq是借鑑了Clojure、Scala、Haskell這些函數式編程語言,引入的一個特殊結構)。它有很是全面的map、filter、groupBy、reduce、find等函數式操做方法。它的Api很強大,你們有興趣能夠去看下。這裏簡單列舉 updateIn/getIn 來展現它帶來的一些便捷操做:

var obj = {
  a: {
    b: {
      list: [1, 2, 3]
    }
  }
};
var map = Immutable.fromJS(obj); // 注意 fromJS這裏實現了深轉換
var map2 = Immutable.updateIn(['a', 'b', 'list'], (list) => {
  return list.push(4);
});

console.log(map2.getIn(['a', 'b', 'list']))
// List [ 1, 2, 3, 4 ]

代碼中咱們要改變數組List的值,沒必要一層一層獲取數據,而是直接傳入對應的路徑修改就行。這種操做在數據嵌套越深時,優點更加明顯。來看下咱們業務代碼的示例吧。

這裏在多個頁面的模塊配置中,要更新某個頁面的某個模塊的數據,咱們只須要在updateIn傳入對應的path和value,就能夠達到預想的效果。篇幅有限,更多的示例請自行查看api。

熟悉React的同窗也基於它結構的不可變性共享性,用它來可以快速進行數據的比較。本來React中使用PureRenderMixin來作DOM diff比較,但只是淺比較,當數據結構比較深的時候,依然會存在多餘的diff過程。這裏只提個點,不深刻展開了,感興趣的同窗能夠自行google。

與 Immutable.js 相似的,還有個seamless-immutable,它的代碼庫很是小,壓縮後下載只有 2K。而 Immutable.js 壓縮後下載有16K。你們各取所需,根據實際狀況,本身斟酌下使用哪一個比較適合。

優缺點

什麼事物都有利弊,代碼庫也不例外。這裏列舉下它們的優缺點,你們權衡利弊,一塊兒來看下:

Normalizr 能夠將數據扁平化處理,方便對深層嵌套的數據進行增刪查改,可是文檔不是很清晰,你們多查多理解,引入庫文件也會增大。Immutable 有持久化數據結構,如List/Map等,併發安全。其次,它支持結構共享,比cloneDeep 性能更優,節省內存。第三,它借鑑了Clojure、Scala、Haskell這些函數式編程語言,引入了特殊結構Seq,支持Lazy operation。Undo/Redo,Copy/Paste,甚至時間旅行這些功能對它來講都是小菜一碟。缺點方面,Immutable源文件過大,壓縮後有15kb。並且它侵入性強,與原生api容易混淆。此外,類型轉換比較繁瑣,尤爲是與服務器交互頻繁時,這種缺點就更加明顯。固然,也能夠根據業務需求,衡量下是否用seamless-immutable,它使用 Object.defineProperty (所以只能在 IE9 及以上使用) 擴展了 JavaScript 的 Array 和 Object 對象來實現,只支持 Array 和 Object 兩種數據類型。可是代碼庫很是小,壓縮後下載只有 2K。

總結

篇幅有限,時間也比較晚了,關於前端數據的扁平化與持久化處理先講這麼多了,有興趣的同窗能夠關注下,後面有時間會多整理分享。

參考資料

相關文章
相關標籤/搜索