標籤:

facebook immutable.js 意義何在,使用場景?

有人用facebook的immutablejs嗎?能不能解釋一下為什麼使用,意義何在?使用場景?


最近項目中頻繁用到所以回答一下。首先,它雖然和React同期出現且跟React配合很爽,但它可不是React工具集里的(它的光芒被掩蓋了),它是一個完全獨立的庫,無論基於什麼框架都可以用它。意義在於它彌補了Javascript沒有不可變數據結構的問題。不可變數據結構是函數式編程中必備的。前端工程師被OOP洗腦太久了,組件根本上就是函數用法,FP的特點更適用於前端開發。

Javascript中對象都是參考類型,也就是a={a:1}; b=a; b.a=10;你發現a.a也變成10了。可變的好處是節省內存或是利用可變性做一些事情,但是,在複雜的開發中它的副作用遠比好處大的多。於是才有了淺copy和深copy,就是為了解決這個問題。舉個常見例子:

var defaultConfig = { /* 默認值 */};

var config = $.extend({}, defaultConfig, initConfig); // jQuery用法。initConfig是自定義值

var config = $.extend(true, {}, defaultConfig, initConfig); // 如果對象是多層的,就用到deep-copy了

ES6出現原生的assign方法,但它相當於是淺copy。如果有了不可變的數據結構就省心了,ES5.1中對象有了freeze方法,也是淺copy,a=Object.freeze({a:1}); b=a; b.a=10; a.a還是1。在實際開發中淺copy通常不夠。如果用immutableJS:

var defaultConfig = Immutable.fromJS({ /* 默認值 */});

var config = defaultConfig.merge(initConfig); // defaultConfig不會改變,返回新值給config

var config = defaultConfig.mergeDeep(initConfig); // 深層merge

上述用deep-copy也可以做到,差別在於性能。每次deep-copy都要把整個對象遞歸的複製一份。而Immutable的實現有些像鏈表,添加一個新結點把舊結點的父子關係轉移到新結點上,性能提升很多,想深挖原理請看這裡:Persistent data structure。ImmutableJS給的遠不止這些,它提供了7種不可變的數據結構:List, Stack, Map, OrderedMap, Set, OrderedSet, Record (詳見文檔Immutable.js,文檔很geek,打開console試吧)。immutableJS + 原生Javascript等於真正的函數式編程。

遍歷對象不再用for-in,可以這樣:

Immutable.fromJS({a:1, b:2, c:3}).map(function(value, key) { /* do some thing */});

實現一個map-reduce:

var o = Immutable.fromJS({a:{a:1}, b:{a:2}, c:{a:3}});

o.map(function(e){ return e.get("a"); }).reduce(function(e1, e2){ return e1 + e2; }, 0);

修改藏在深處的值,可以這樣:

var o = Immutable.fromJS({a:[{a1:1}, {b:[{t:1}]}, {c1:2}], b:2, c:3});

o = o.setIn(["a", 1, "b", 0, "t"], 100); // t賦值

o = o.updateIn(["a", 1, "b", 0, "t"], function(e){ return e * 100; }); // t * 100

比較兩個對象是否完全相等: o1.equals(o2)

遠不止這些,immutableJS提供了強大的api自己去看吧。由於是不可變的,可以放心的對對象進行任意操作。在React開發中,頻繁操作state對象或是store,配合immutableJS快、安全、方便。


寫了篇文章介紹這個事情,歡迎大家去看,搞定immutable.js

什麼是Immutable Data

Immutable Data是指一旦被創造後,就不可以被改變的數據。

通過使用Immutable Data,可以讓我們更容易的去處理緩存、回退、數據變化檢測等問題,簡化我們的開發。

js中的Immutable Data

在javascript中我們可以通過deep clone來模擬Immutable Data,就是每次對數據進行操作,新對數據進行deep clone出一個新數據。

deep clone

/**
* learning-immutable - clone-deep.js
* Created by mds on 15/6/6.
*/

"use strict";
var cloneDeep = require("lodash.clonedeep");

var data = {
id: "data",
author: {
name: "mdemo",
github: "https://github.com/demohi"
}
};

var data1 = cloneDeep(data);

console.log("equal:", data1===data); //false

data1.id = "data1";
data1.author.name = "demohi";

console.log(data.id);// data
console.log(data1.id);// data1

console.log(data.author.name);//mdemo
console.log(data1.author.name);//demohi

當然你或許意識到了,這樣非常的慢。如下圖,確實很慢

主角immutable.js登場

immutable.js是由facebook開源的一個項目,主要是為了解決javascript Immutable Data的問題,通過參考hash maps tries 和 vector tries提供了一種更有效的方式。

簡單的來講,immutable.js通過structural sharing來解決的性能問題。我們先看一段視頻,看看immutable.js是如何做的

當我們發生一個set操作的時候,immutable.js會只clone它的父級別以上的部分,其他保持不變,這樣大家可以共享同樣的部分,可以大大提高性能。

為什麼你要在React.js中使用Immutable Data

熟悉React.js的都應該知道,React.js是一個UI = f(states)的框架,為了解決更新的問題,React.js使用了virtual dom,virtual dom通過diff修改dom,來實現高效的dom更新。

聽起來很完美吧,但是有一個問題。當state更新時,如果數據沒變,你也會去做virtual dom的diff,這就產生了浪費。這種情況其實很常見,可以參考flummox這篇文章

當然你可能會說,你可以使用PureRenderMixin來解決呀,PureRenderMixin是個好東西,我們可以用它來解決一部分的上述問題,但是如果你留心的話,你可以在文檔中看到下面這段提示。

This only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only mix into components which have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

PureRenderMixin只是簡單的淺比較,不使用於多層比較。那怎麼辦??自己去做複雜比較的話,性能又會非常差。

方案就是使用immutable.js可以解決這個問題。因為每一次state更新只要有數據改變,那麼PureRenderMixin可以立刻判斷出數據改變,可以大大提升性能。這部分還可以參考官方文檔Immutability Helpers

總結就是:使用PureRenderMixin + immutable.js

參考

React.js Conf 2015 - Immutable Data and React

Immutability Helpers

PureRenderMixin

immutable-js


最近在移動端使用 React,在複雜的頁面里發現 vdom re-render 有性能優化的空間,於是了解到了 immutable.js。所以也來回答一下這個問題。

如 @張克軍 所說:

  • 它是一個完全獨立的庫,無論基於什麼框架都可以用它。意義在於它彌補了Javascript 沒有不可變數據結構的問題。

  • 在React開發中,頻繁操作 state 對象或是 store,配合 immutableJS 快、安全

克軍說的第一點:函數式編程中的不可變數據結構問題,可參考 @徐飛 的文章《2015前端組件化框架之路》中 Immutable Data 章節。

我就第二點來展開說說 immutableJS 的使用場景:在 React 中使用 immutableJS (Immutable as React state · facebook/immutable-js Wiki · GitHub)。

從問題說起:

熟悉 React 組件生命周期的話都知道:調用 setState 方法總是會觸發 render 方法從而進行 vdom re-render 相關邏輯,哪怕實際上你沒有更改到 Component.state 。

```javascript

this.state = {count: 0}

this.setState({count: 0});// 組件 state 並未被改變,但仍會觸發 render 方法

```

為了避免這種性能上的浪費,React 提供了一個 shouldComponentUpdate 來控制觸發 vdom re-render 邏輯的條件。於是 PureRenderMixin 作為一種優化技巧被使用。

這樣就完美了?並不是。去趴一趴 PureRenderMixin 的源碼( react/shallowEqual.js at master · facebook/react · GitHub )就會發現它僅僅是淺比較對象,深層次的數據結構根本不管用。這裡有一個示例:JS Bin1,點擊 SetToSameCount ,會發現 render times 沒有變化,再點 SetToSameSchool ,render times 增加了。

這時候 immutableJS 就派得上用場了:

```javascript

var map1 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});

var map2 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});

Immutable.is(map1, map2); // true

```

當然上述功能通過 deep-diff 也可以做到(e.g. flitbit/diff · GitHub),性能差別上我暫時沒有去進行測試。

但是,如果僅僅是為了一個比較而引入 Immutable 的話,就太大材小用了。Immutable 的功能遠不止如此。

例如,前面的 JS Bin1 示例中的場景:(在 Hight school name 的 input 內中輸入觸發 handleChangeSchool 就會發現 middle 屬性被刪除了)

這樣調用 setState 會將 middle 屬性給覆蓋掉(setState 內部是淺 merge)。在實際項目中,我們將會頻繁地操作 state 或 store ,這時候配合 Immutable 就快且安全多了:

看一個上面的示例怎樣用 Immutable 重寫一遍:JS Bin2

快:

  • 便捷的API:```d.updateIn(["user","school", "high"], ()=&>v)``` 更多參考:Immutable.js

  • 更好的性能:facebook/immutable-js · GitHub (They are highly efficient on modern JavaScript VMs by using structural sharing via hash maps tries and vector tries as popularized by Clojure and Scala, minimizing the need to copy or cache data.) (暫時未給出性能測試)

安全:是指使用上的安全,所有刪改操作都是增量的。如 JS Bin2 示例中更新 school high name 所示,並沒有影響到 middle 。

這就是 Immutable 在我們項目中的使用場景了。


mutable data structure做diff需要遍歷

immutable只要對比一下reference是不是一個就行了

react做virtual dom的diff會快


The case for Immutability

一般來說監測數據變化時可以用 Object.observe,或者自己去遍曆數據結構進行對比。這樣計算差異時可能做很多不必要的檢查。Immutable 把事情提前做了,在修改數據的時候就檢查差異,不變返回原引用,有變化則直接返回一個新的引用。這樣檢查任何數據都只要簡單地用 `===` 對比就行了。這東西應該也是配合 Facebook 自家的 React 框架和 Flux 架構來用的。


用最近我翻譯的 Lee Byron 寫的 Why Invest in Tools 來回答這個問題太合適了:「為什麼要造輪子?」為什麼要造輪子? // Nextoffer Blog

-------- 以下是正文 -------

前幾天在 React-Europe 大會上,我分享了一個我花了三年多時間的項目 - GraphQL.

會議結束後,不少參會者問我:

Facebook 是怎麼做到一直保持產出這些「反思當前最佳實踐」的新技術的?

既然這是 React 大會,那麼就讓我們從 React 開始講起吧。

Photo: Rasmus Andersson

兩年前

兩年前我們開源 React 的時候,這一直是被 JavaScript 社區取笑的對象;甚至 Facebook 內部(包括我自己)都不認為這是一個好想法。Jordan Walke 的執著和理想主義最終還是對大家產生了影響。最早我們以為他瘋了,不過他的確是個瘋子,但他也確實發現了一些什麼。現在,我們看到 React 已經改變了我們在各種平台上「造」東西的方式。Adam Ernst 借鑒了 Jordan 的一些想法,然後「造」了 ComponentKit for iOS. 當然,我們自己的 iOS 組剛接觸她的時候也是充滿了猜疑;但再一次,ComponentKit 很大程度地改變了我們「造」iOS 程序的方式。

React 和 ComponentKit 都是 Facebook 內部個人自主發起的項目。事實上當時這些項目的方向和工程師團隊原有的開發方式都是相反的。React 直接挑戰我們當時非常看好的一些 JS 框架。其實剛開始開發 ComponentKit 的時候我們內部就已經「造」並且在使用了的一些 iOS UI 框架。

其他的工具並沒有問題,也不差(話說回來他們其實很贊)但他們也不是完美的。

他們各自都有著利弊權衡,都有自己的優勢和劣勢。只有在一個自由開發環境的情況下,工程師才能去「造」一些他們認為更高效幫助他們完成工作的工具。

工程師的冒險文化

在 Facebook,我們不僅僅讓,更是鼓勵,工程師做這些好玩的「實驗」。其實這些項目還是存在一定風險的,而且也不是很吸引人,也常常失敗(需要改)。然後你會發現像 React, ComponentKit, HHVM, GraphQL, Immutable.js, Flow, Pop, 和 AsyncDisplayKit 這樣的「實驗」。這些都是值得去冒的險。對於像 Facebook 這樣擁有強大的工程團隊的公司來說,其中一個優勢是可以充分地讓工程師們去嘗試這些實驗,而不是盯著 scrum 或者為了公司的短期業績來工作。

上面提到的每一個項目都遇到過非常強烈的反對。有些人(有時候甚至是我)會想讓一些項目早些承認失敗。然而他們並沒有停止。Facebook 不僅有很好的工程師管理哲學,而且有非常棒的管理層 - 他們知道相信工程師們的重要性。就算項目遇到了同事的反對,就算也未知項目的價值所在,就算還有更重要的事情可以去做,Facebook 的管理層信任他們的工程師去冒一些值得冒的險,同時專註在他們相信能夠產生影響的領域。

我的小組 - Product Infrastructure,和大多數的 Facebook 小組一樣都有相同的哲學:工程師對世界的影響不止於公司的產品。上面提到的開源項目都有著很強的社區,每個開源都對整個互聯網/軟體行業有著深刻的影響。開源不僅僅是一個公益理想化的東西,她還是我們如何學習和展示我們的工作啟發的影響的重要組成部分。

健康的開源環境在招聘環節也是非常有利的。一些我面試過的求職者對我說,他們對 Facebook 的關注是因為看到了 React, AsyncDisplayKit, Pop, 這些項目;並且想參與到這些項目中去。這些項目吸引了非常聰明的人才進來,從而自然地產生一個良性循環。

Success is not found in isolation

隨著項目變得越來越有意思,她的潛力被更多的人看到,團隊組建 - 然後一個雪球效應自然地推進了一整個項目。在 Facebook,工程師做著與自己職份外的項目並不罕見;或者從一個小組調到其他小組都非常常見;而這樣的文化讓這個雪球可以滾起來。這也意味著每個項目後面有許多無名功臣。

在這裡我想點名一些(遠遠少於全部成員)早期為 GraphQL 做出貢獻的人:Nick Schlock, Daniel Schafer, 和我自己。

  • Beau Hartshorne 是 GraphQL 不可缺少的催化劑。他準確定位並指明了問題所在,找到了對的人,而且激發了我們去找解決問題的方案。Sometimes it』s hard to see the forest through the trees, and Beau』s a rare person who is always looking at the forest.

  • Jonathan Dann 和 David Renie 是兩位推動第一版 GraphQL 的 iOS 工程師。是他們做了非常大量的工作把 GraphQL 整合進 News Feed. 他們也協助建立了一些我們一直沿用到今天的非常重要的基礎設施。

  • Rasmus Andersson 用全新視角想像到一種不一樣的方式在移動應用中傳輸數據;而這種方式成為了我們 Android SDK 的基礎。他的一些想法還激發了 Relay - 用 GraphQL「造」web 端應用的工具。

  • 另外兩位 GraphQL 組早期成員,Nathaniel Roman and Charles Ma, 幫助開發了 GraphQL 客戶端工具。

  • Scott Wolchok 一手組織和改善了 GraphQL 的 iOS 和其他跨平台的客戶端工具的數據模型。他的嚴謹的思路啟發了我們去研究最新 cross-cutting 的進展。

到今天,已經有一個成熟的小組專門支持和投入到 GraphQL, 伺服器,客戶端工具,和 Facebook 的類型系統。

我們的使命

正是因為我們對持續產出長期價值的專註,讓 Facebook 能夠一直「造」出一些「反思當前最佳實踐」的技術,且在業內引起不小的影響。我們敢去試錯;我們相信工程師能去做正確的事。當一些「實驗」看起來有點兒意思的時候,充滿想法和聰明的人會自發地聚到一起來實現這個「實驗」。

在 Facebook, 我們的職責不僅僅是「造」Facebook,還是讓世界變得更加的開放和連接。而我們這個 Product Infrastructure 小組通過開源這些工具來幫助我們完成這個使命。

本文已獲得作者翻譯以及傳播許可

轉載請註明來源


除了帶來幾種操作方便的數據結構和API,我再詳細說一下可維護性和性能上的提升:

  • 在按引用來傳遞數據的場景中,存在因修改數據而帶來影響範圍不可控的副作用,因為你不知道誰還引用著這份數據,不知道你的修改會影響到誰,而Immutable.js能解決這個問題,能控制修改帶來的影響範圍,因為它每次修改都會創建一個新的對象,原對象不變。

    比如,調用函數func(objData),如果objData是Immutable.js包裝過的,func內部在修改objData時不必擔心會影響func調用者所擁有的數據,同樣,func調用者也可以在調用func之後隨意修改objData,不必擔心會影響func內部所擁有的數據。

    舉個反例,Backbone.js中的不少方法的最後一個參數都是options對象,用來支持對方法內部操作進行一些配置,但Backbone.js會在方法內部修改這個options對象,甚至作為事件回調函數的參數傳遞出去,一個對象在Backbone.js框架內部外部傳進傳出,修改這個對象時由於影響範圍不可控,很可能會驚呼「我cao~誰改了我的數據?」。

    類似地,Flux架構中數據也會經過多層的傳遞,component=&>actions(其中可能有多層middleware)=&>store,而多個component組成的樹狀結構也需要將數據一層層往子component傳遞。這種按引用傳遞數據,數據流經多層的場景,如果沒有將引用類型的數據Immutable化,拿到一份引用類型的數據,修改起來心裡總是不踏實。

    這是Immutable.js在代碼可維護性上帶來的提升。

  • 避免這個副作用的一種實現是按值傳遞,也就是拷貝一份再傳遞過去,有深層結構就深拷貝。深拷貝在只做局部修改的時候做了很多無用功,於是Immutable.js做了性能優化。網上找了個圖,假如我們要修改左圖中黃色節點的子節點4,那麼Immutable.js只需要更新右圖中的綠色節點,其餘節點不需拷貝,繼續復用。也就是說,Immutable.js會更新從根節點到所修改節點路徑上的所有節點,由於修改了根節點,所以返回一個新對象,這也解釋了為什麼能控制副作用。

跟React.js的配合使用中提高性能:

1. 假如你在組件state中保存了一份有深層結構的引用類型的數據,如果沒有Immutable.js,你需要深拷貝一份再做修改。而用Immutable.js將state中的數據包裝一下,不需深拷貝就可以直接修改。

2. 由於修改後返回的是新對象,React.js只需要在oldState.obj === newState.obj這一層就能判斷出obj產生了變化,不需要深入obj的深層結構。------------------

P.S. 也不能光說好的,吐槽一下,Immutable.js設計的API很細很多,帶來幾種數據結構概念有差別但差別不是很大,庫的體積很大,導致基本跟移動端無緣,要是能夠將幾種數據結構模塊化一下,按需打包就好了,類似這樣let ImmutableList from "immutable/list"


寫了一篇關於immutable的文章,從以下幾點進行了介紹

- Immutable產生的背景

- Immutable的好處

- 在React中引入帶來的利弊

文章: Immutable.js及在React中的應用


寫了篇關於這個庫的專欄。 稍微看了下 Immutablejs - 前端小記 - 知乎專欄

總得來說, Immutable 字義上只是做了不可修改的結構,但其實這個庫提供的是安全的、便利的、高性能的數據結構與演算法庫。

對於有計算需求的項目而言應該很有用。


我說說為什麼要有這個庫吧,實際上對於immutable object的需求還是很廣泛的。很多時候在對象傳遞的時候我們應該copy而不是直接傳遞引用。

對於Redux來說,immutable的意義在於讓state變得comparable與reliable。

如果非immutable,每次狀態改變直接改變state的對象,那麼相當於你任何時候使用的state都可能被隨時改變,這是非常可怕的事情。在設計裡面也是不合理的,出了bug也非常難以追蹤。正確的做法是每次改變state的時候返回一個新的state,並使用新的state通知應用做出改變。

在另一個方面,直接對原state修改的話你就很難直接對比修改前後的對象,而如果每次改變都是新的object(immutable)的話就有了新、舊兩個對象做對比,很容易達到diff並增量更新的效果。

提供兩個redux的issue供參考:

Why cant state be mutated.... · Issue #758 · reactjs/redux · GitHub

Cost of immutability · Issue #328 · reactjs/redux · GitHub

ImmutableJS,就是讓object copy變得更加快捷與方便。


可以看下這個:

Keynote: The Value of Values

immutable.js 誕生是借鑒了Clojure語言的immutable data structures思想。

https://scott.mn/2014/04/27/why_immutable_collections/

Clojure語言的作者講述了PLace-Oriented Programming和value-oriented programming 的思想上的區別,感興趣的可以看看,很有意思。


推薦閱讀:

如果寫一個靜態HTML頁面,直接寫HTML代碼和用JS動態生成代碼,哪種方式要好點?為什麼?
JS 引擎如何實現 for (let i;...) { } 寫法中每次循環綁定不同的循環變數 i?
gulp是一個前端工作流管理工具,具體做什麼的?
如何看待『真阿當』關於前端核心壁壘的描述?
CSS 動畫會不會被 JS 阻塞?

TAG:JavaScript |