不可變JavaScript庫(Immutable JS)值不值得用?
導語
我是一個前端開發人員,擁有四年工作經驗,目前在一個大型軟體團體裡工作,製作一個以React框架和Redux庫為基礎建立起來的新單頁程式。
創作一個前所未有的網站,這對所有開發人員而言都有著令人興奮的前景。我們會天真地眨著大眼睛,滿滿地抓起一把新技術,把它們全投入到這個node平臺伺服器上去,再抽身引退,對自己那領先時代的天賦驚歎不已。
選擇的技術之中,有一個是Facebook公司的“Immutable”軟體庫。我們準備利用這個庫來實現資料的表現方式,加強資料的不可變性(immutability),以此為開始,建立起面向功能的程式設計模式。這篇帖子就是要對其進行一次審視。
不可變資料與Redux庫
不可變資料是面向功能程式設計(functional programming)的核心概念,這種概念在JavaScript中的應用已漸佔優勢。使用React框架和Redux庫時,不可變資料能幫助鞏固這兩者的核心原則:如果程式狀態(app state)沒有發生改變,那網頁的文件物件模型(DOM)也不用改變。
不少文章已經寫到過使用不可變資料的優點,主要包括:
- 簡化貫穿程式的資料流
- 不再需要資料複製的防禦機制
- 優化對資料變化的檢測
- 通過記憶化(memoization)技術提高程式效能
Immutable庫
Immutable庫是Facebook公司的一個開源軟體庫。我們使用redux-immutable模組將這個庫整合進我們的程式,這樣我們就能以Immutable庫提供的資料型別來儲存程式狀態(app state)了。
要將程式狀態(app state)渲染成網頁,我們得把狀態資料從Redux的儲存物件(store)中轉移到React元件裡去。這是通過react-redux模組的“connect()”修飾函式來實現的。
在程式開發過程中,我們注意到了以下優點和缺點。
[優點]強化了不可變性
不管選用哪個庫,使用不可變資料型別的頭一條理由肯定是能夠保證做專案的人不能違反不可變原則。
嚴格地說,”不可變(Immutable)“庫有助於簡化開發過程,因為大家不再需要在程式碼中追蹤資料,尋找資料變更的位置。不可變資料型別取而代之,能始終精確表現當前儲存物件(store)中儲存的程式狀態(app state)。
有了這個庫,我們就能發揮上述不可變資料型別的優點,似乎沒什麼不好的。然而,缺點也確實存在,而且等到開發工作正式開始時,這些缺點才顯露了出來。
[缺點]文件與除錯
Facebook給前端開發人員提供的不僅僅是一個軟體框架,而是整個程式製作的軟體生態系統。然而,和React之類的框架比起來,”不可變(Immutable)“庫的文件極其不完整。
不清楚”不可變(Immutable)“庫句法,或者程式碼無法像預想的那樣起作用時,開發人員都會求助於文件,不過常常是看了還不明白。程式碼為什麼不對?既然看了還不明白,最終大家都會使用終端日誌”console.log()“大法。不過很可惜,用日誌審查資料時會發現自己一直在自定義資料型別的屬性裡翻來翻去。
終端日誌列印出來的Immutable庫物件
要解決這個問題,可以在任何Immutable庫的物件上呼叫”toJS()“函式,把物件轉換成一個純JavaScript物件,再列印出來。但這類小問題會減緩開發速度,要是文件能再完善點,情況就會更好些。
不管怎麼樣,如果僅僅為了確定當前有什麼資料就要看文件、作除錯,那作為製作程式的基礎來說真不怎麼樣。
[缺點]有反模式化的酸腐氣息
我們可以通過”connect()“修飾函式,從程式的儲存物件(store)中取得資料,以此訪問”不可變(Immutable)“庫的資料物件。但我們團隊以前通常會用原生資料型別寫元件。為了轉換資料,我們開發了一個模式,在”connect()“修飾函式中用了”toJS()“函式,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 從儲存物件(store)裡獲取資料 [@connect](http://twitter.com/connect "Twitter profile for @connect")((state) => { // 將儲存物件(store)資料轉換成原生JavaScript物件 user: state.get(”user“).toJS(), wine: state.getIn([”drinks“, ”wines“]).toJS() }) class HelloWine extends Component { render() { // 用ES6版本格式把屬性(props)裡的資料解構出來 const { user, wines: { houseRed } } = this.props return <div>{`Hi ${ user }! Fancy some ${ houseRed }?`}</div> } } |
這個模式看起來很方便也很安全,但用在移動裝置上時,我們發現啟動Redux的行為(actions)功能慢得受不了。下面是在三星S5上開啟程式側邊選單時記錄下來的JavaScript效能剖析。
低效渲染剖析
開啟選單用了2秒多,對一個用前沿技術做的網站程式來說可不怎麼爽!
我們對程式執行進行了追蹤,發現上面寫的那個模式就是問題所在。在後臺發生的情況是Redux把行為物件(action)傳送到儲存物件(store),然後用”reducer()“函式產生的新狀態(state)更新儲存物件(store)。
元件用”connect()“函式修飾以後,每次都會檢查資料是否更新。資料有更新,元件才會通過React生命週期觸發重渲染。這使Redux庫能選擇性地渲染React框架元件,提升效能。
每次執行”connect()“函式時,通過”toJS()“函式,程式狀態(app state)都被轉換成了一個原生JavaScript物件,每次都會產生一個新的物件。因此和之前的狀態相比,即使當前的”不可變(Immutable)“庫物件沒有變化,產生的物件仍然是不同的。換句話說,任何行為(action)發動時,每個用”connect()“函式修飾的元素以及子元素都會被重新渲染過。
如果別的都記不住,那記住這點:toJS()函式絕對不要在”connect()“修飾函式中呼叫。
[缺點] 不怎麼符合ES6版本的格式
如果程式狀態(app state)儲存在”不可變(Immutable)“庫資料型別中的話,那我們的元件也應該運用同樣的資料型別,就這樣決定了。於是我們照此重組了程式碼,卻產生了一個很大的缺陷,那就是原生功能的缺失。
比如,ES6的解構(de-structuring)功能現在就變成了幾個”get()“函式和”getIn()“函式呼叫的結合。
1 |
const { wines: { houseRed: { name, year } } } = this.props |
1 2 3 4 |
// 變成 const { wines } = this.props const name = wines.getIn([”houseRed“, ”name“]) const year = wines.getIn([”houseRed“, ”year“]) |
程式碼變長了,沒那麼漂亮了。而且個人而言,我不喜歡用那麼多字串,因為如果打錯一個字,本來程式會丟擲JavaScript錯誤,提醒錯誤所在,現在能得到的只是一個“未定義”,而真正的問題可能無法發現。
另外,ES6版本的展開句法(spreading)功能也丟失了,這會使屬性重新賦值的語句變得很冗長。
1 2 |
<AmazingComponent { …props } /> <AmazingComponent prop1={ props.prop1 } prop2={ props.prop2 } prop3={ props.prop3 } /> |
這些”不可變(Immutable)“庫句法的缺點觸及到了我們的痛處,又讓我想起了為什麼一開始要做那個轉換模式。如果核心資料型別處理出了問題,即使都是些小問題,也會讓人感到沮喪,又會浪費更多寶貴的開發時間。
結果
為了使用”不可變(Immutable)“庫,我們重組了一些元件的程式碼。然後我們重新評估了目前所處的局面,討論了上面講的那些方面,結論就是”不可變(Immutable)“庫唯一的好處就是能強化不可變性,但意義何在?面向功能程式設計真正的意思是大家不要嘗試去修改狀態,所以狀態的具體資料型別是不是可變只是個技術問題,和思路沒有關係。
在使用”不可變(Immutable)“庫過程中我們考慮了所有的缺點,最終決定把它從專案裡完全移除。只要遵循面向功能程式設計的原則,我們就有信心處理自己的資料。我們對開發人員的信任,加上相互之間的程式碼審查已經足夠保證不犯低階錯誤了。
感謝閱讀,希望有所幫助!