更簡單,更快速的建立不可變數據類型前端
做者:張釗git
在 JS 中對象的使用須要格外注意引用問題,斷絕引用的方式常見有深拷貝。可是深拷貝相比較而言比較消耗性能。本文主要簡介 immutable-js 和 immer 兩個處理「不可變數據」的庫,同時簡單分析了 immer 的實現方式,最後經過測試數據,對比總結了 immutable-js 和 immer 的優劣。
JS 裏面的變量類型能夠分爲 基本類型 和 引用類型 。github
在使用過程當中,引用類型常常會產生一些沒法意識到的反作用,因此在現代 JS 開發過程當中,有經驗的開發者都會在特定位置有意識的寫下斷開引用的不可變數據類型。編程
// 由於引用所帶來的反作用: var a = [{ val: 1 }] var b = a.map(item => item.val = 2 // 指望:b 的每個元素的 val 值變爲 2,但最終 a 裏面每一個元素的 val 也變爲了 2 console.log(a[0].val) // 2
從上述例子咱們能夠發現,本意是隻想讓 b 中的每個元素的值變爲 2 ,但卻無心中改掉了 a 中每個元素的結果,這固然是不符合預期的。接下來若是某個地方使用到了 a ,很容易發生一些咱們難以預料而且難以 debug 的 bug (由於它的值在不經意間被改變了)。數組
在發現這樣的問題以後,解決方案也很簡單。通常來講當須要傳遞一個引用類型的變量(例如對象)進一個函數時,咱們可使用 Object.assign 或者 ... 對對象進行解構,成功斷掉一層的引用。安全
例如上面的問題咱們能夠改用下面的這種寫法:服務器
var a = [{ val: 1 }] var b = a.map(item => ({ ...item, val: 2 })) console.log(a[0].val) // 1 console.log(b[0].val) // 2
可是這樣作會有另一個問題,不管是 Object.assign 仍是 ... 的解構操做,斷掉的引用也只是一層,若是對象嵌套超過一層,這樣作仍是有必定的風險。網絡
// 深層次的對象嵌套,這裏 a 裏面的元素對象下又嵌套了一個 desc 對象` var a = [{ val: 1, desc: { text: 'a' } }] var b = a.map(item => ({ ...item, val: 2 })) console.log(a === b) // false console.log(a[0].desc === b[0].desc) // true b[0].desc.text = 'b'; // 改變 b 中對象元素對象下的內容 console.log(a[0].desc.text); // b (a 中元素的值無心中被改變了)
a[0].desc === b[0].desc 表達式的結果仍爲 true,這說明在程序內部 a[0].desc 和 b[0].desc 仍然指向相同的引用。若是後面的代碼一不當心在一個函數內部直接經過 b[0].desc 進行賦值,就必定會改變具備相同引用的 a[0].desc 部分的結果。例如上面的例子中,經過「點」直接操做 b 中的嵌套對象,最終也改變了 a 裏面的結果。數據結構
因此在這以後,大多數狀況下咱們會考慮 深拷貝 這樣的操做來徹底避免上面遇到的全部問題。深拷貝,顧名思義就是在遍歷過程當中,若是遇到了可能出現引用的數據類型(例如前文中舉例的對象 Object),就會遞歸的徹底建立一個新的類型。ide
// 一個簡單的深拷貝函數,只作了簡單的判斷
// 用戶態這裏輸入的 obj 必定是一個 Plain Object,而且全部 value 也是 Plain Object
function deepClone(obj) { const keys = Object.keys(obj) return keys.reduce((memo, current) => { const value = obj[current] if (typeof value === 'object') { // 若是當前結果是一個對象,那咱們就繼續遞歸這個結果 return { ...memo, [current]: deepClone(value), } } return { ...memo, [current]: value, } }, {}) }
用上面的 deepClone 函數進行簡單測試
var a = { val: 1, desc: { text: 'a' }, }; var b = deepClone(a) b.val = 2 console.log(a.val) // 1 console.log(b.val) // 2 b.desc.text = 'b' console.log(a.desc.text) // 'a' console.log(b.desc.text) // 'b'
上面的這個 deepClone 能夠知足簡單的需求,可是真正在生產工做中,咱們須要考慮很是多的因素。
舉例來講:
由於有太多不肯定因素,因此在真正的工程實踐中,仍是推薦你們使用大型開源項目裏面的工具函數。比較經常使用的爲你們所熟知的就是 lodash.cloneDeep,不管是安全性仍是效果都有所保障。
其實,這種去除引用數據類型反作用的數據的概念咱們稱做 immutable,意爲不可變的數據,其實理解爲不可變關係更爲恰當。每當咱們建立一個被 deepClone 過的數據,新的數據進行有反作用 (side effect) 的操做都不會影響到以前的數據,這也就是 immutable 的精髓和本質。
這裏的反作用不僅侷限於經過「點」操做對屬性賦值。例如 array 裏面的 push, pop , splice 等方法操做都是會改變原來的數組結果,這些操做都算是非 immutable。相比較而言,slice , map 這類返回操做結果爲一個新數組的形式,就是 immutable 的操做。
然而 deepClone 這種函數雖然斷絕了引用關係實現了 immutable,可是相對來講開銷太大(由於他至關於徹底建立了一個新的對象出來,有時候有些 value 咱們不會進行賦值操做,因此即便保持引用也不要緊)。
因此在 2014 年,facebook 的 immutable-js 橫空出世,即保證了數據間的 immutable ,在運行時判斷數據間的引用狀況,又兼顧了性能。
immutable-js 簡介
immutable-js 使用了另外一套數據結構的 API ,與咱們的常見操做有些許不一樣,它將全部的原生數據類型(Object, Array等)都會轉化成 immutable-js 的內部對象(Map,List 等),而且任何操做最終都會返回一個新的 immutable 的值。
上面的例子使用 immutable-js 就須要這樣改造一下:
const { fromJS } = require('immutable') const data = { val: 1, desc: { text: 'a', }, } // 這裏使用 fromJS 將 data 轉變爲 immutable 內部對象 const a = fromJS(data) // 以後咱們就能夠調用內部對象上的方法如 get getIn set setIn 等,來操做原對象上的值 const b = a.set('val', 2) console.log(a.get('val')) // 1 console.log(b.get('val')) // 2 const pathToText = ['desc', 'text'] const c = a.setIn([...pathToText], 'c') console.log(a.getIn([...pathToText])) // 'a' console.log(c.getIn([...pathToText])) // 'c'
對於性能方面,immutable-js 也有它的優點,舉個簡單的例子:
const { fromJS } = require('immutable') const data = { content: { time: '2018-02-01', val: 'Hello World', }, desc: { text: 'a', }, } // 把 data 轉化爲 immutable-js 中的內置對象 const a = fromJS(data) const b = a.setIn(['desc', 'text'], 'b') console.log(b.get('desc') === a.get('desc')) // false // content 的值沒有改動過,因此 a 和 b 的 content 還保持着引用 console.log(b.get('content') === a.get('content')) // true // 將 immutable-js 的內置對象又轉化爲 JS 原生的內容 const c = a.toJS() const d = b.toJS() // 這時咱們發現全部的引用都斷開了 console.log(c.desc === d.desc) // false console.log(c.content === d.content) // false
從上面的例子能夠看出來,咱們操做 immutable-js 的內置對象過程當中,改變了 desc 對象下的內容。但實際上 content 的結果咱們並無改動。咱們經過 === 進行比較的過程當中,就能發現 desc 的引用已經斷開了,可是 content 的引用還保持着鏈接。
在 immutable-js 的數據結構中,深層次的對象 在沒有修改的狀況下仍然可以保證嚴格相等,這也是 immutable-js 的另外一個特色 「深層嵌套對象的結構共享」。即嵌套對象在沒有改動前仍然在內部保持着以前的引用,修改後斷開引用,可是卻不會影響以前的結果。
常用 React 的同窗確定也對 immutable-js 不陌生,這也就是爲何 immutable-js 會極大提升 React 頁面性能的緣由之一了。
固然可以達到 immutable 效果的固然不僅這幾個個例,這篇文章我主要想介紹實現 immutable 的庫實際上是 immer。
immer 簡介
immer 的做者同時也是 mobx 的做者。mobx 又像是把 Vue 的一套東西融合進了 React,已經在社區取得了不錯的反響。immer 則是他在 immutable 方面所作的另外一個實踐。
與 immutable-js 最大的不一樣,immer 是使用原生數據結構的 API 而不是像 immutable-js 那樣轉化爲內置對象以後使用內置的 API,舉個簡單例子:
const produce = require('immer') const state = { done: false, val: 'string', } // 全部具備反作用的操做,均可以放入 produce 函數的第二個參數內進行 // 最終返回的結果並不影響原來的數據 const newState = produce(state, (draft) => { draft.done = true }) console.log(state.done) // false console.log(newState.done) // true
經過上面的例子咱們能發現,全部具備反作用的邏輯均可以放進 produce 的第二個參數的函數內部進行處理。在這個函數內部對原來的數據進行任何操做,都不會對原對象產生任何影響。
這裏咱們能夠在函數中進行任何操做,例如 push splice 等非 immutable 的 API,最終結果與原來的數據互不影響。
Immer 最大的好處就在這裏,咱們的學習沒有太多成本,由於它的 API 不多,無非就是把咱們以前的操做放置到 produce 函數的第二參數函數中去執行。
immer 原理解析
Immer 源碼中,使用了一個 ES6 的新特性 Proxy 對象。Proxy 對象容許攔截某些操做並實現自定義行爲,但大多數 JS 同窗在平常業務中可能並不常用這種元編程模式,因此這裏簡單且快速的介紹一下它的使用。
Proxy 對象接受兩個參數,第一個參數是須要操做的對象,第二個參數是設置對應攔截的屬性,這裏的屬性一樣也支持 get,set 等等,也就是劫持了對應元素的讀和寫,可以在其中進行一些操做,最終返回一個 Proxy 對象實例。
const proxy = new Proxy({}, { get(target, key) { // 這裏的 target 就是 Proxy 的第一個參數對象 console.log('proxy get key', key) }, set(target, key, value) { console.log('value', value) } }) // 全部讀取操做都被轉發到了 get 方法內部 proxy.info // 'proxy get key info' // 全部設置操做都被轉發到了 set 方法內部 proxy.info = 1 // 'value 1'
上面這個例子中傳入的第一個參數是一個空對象,固然咱們能夠用其餘已有內容的對象代替它,也就是函數參數中的 target。
immer 的作法就是維護一份 state 在內部,劫持全部操做,內部來判斷是否有變化從而最終決定如何返回。下面這個例子就是一個構造函數,若是將它的實例傳入 Proxy 對象做爲第一個參數,就可以後面的處理對象中使用其中的方法:
class Store { constructor(state) { this.modified = false this.source = state this.copy = null } get(key) { if (!this.modified) return this.source[key] return this.copy[key] } set(key, value) { if (!this.modified) this.modifing() return this.copy[key] = value } modifing() { if (this.modified) return this.modified = true // 這裏使用原生的 API 實現一層 immutable, // 數組使用 slice 則會建立一個新數組。對象則使用解構 this.copy = Array.isArray(this.source) ? this.source.slice() : { ...this.source } } }
上面這個 Store 構造函數相比源代碼省略了不少判斷的部分。實例上面有 modified,source,copy 三個屬性,有 get,set,modifing 三個方法。modified 做爲內置的 flag,判斷如何進行設置和返回。
裏面最關鍵的就應該是 modifing 這個函數,若是觸發了 setter 而且以前沒有改動過的話,就會手動將 modified 這個 flag 設置爲 true,而且手動經過原生的 API 實現一層 immutable。
對於 Proxy 的第二個參數,在簡版的實現中,咱們只是簡單作一層轉發,任何對元素的讀取和寫入都轉發到 store 實例內部方法去處理。
const PROXY_FLAG = '@@SYMBOL_PROXY_FLAG' const handler = { get(target, key) { // 若是遇到了這個 flag 咱們直接返回咱們操做的 target if (key === PROXY_FLAG) return target return target.get(key) }, set(target, key, value) { return target.set(key, value) }, }
這裏在 getter 裏面加一個 flag 的目的就在於未來從 proxy 對象中獲取 store 實例更加方便。
最終咱們可以完成這個 produce 函數,建立 store 實例後建立 proxy 實例。而後將建立的 proxy 實例傳入第二個函數中去。這樣不管在內部作怎樣有反作用的事情,最終都會在 store 實例內部將它解決。最終獲得了修改以後的 proxy 對象,而 proxy 對象內部已經維護了兩份 state ,經過判斷 modified 的值來肯定究竟返回哪一份。
function produce(state, producer) { const store = new Store(state) const proxy = new Proxy(store, handler) // 執行咱們傳入的 producer 函數,咱們實際操做的都是 proxy 實例,全部有反作用的操做都會在 proxy 內部進行判斷,是否最終要對 store 進行改動 producer(proxy) // 處理完成以後,經過 flag 拿到 store 實例 const newState = proxy[PROXY_FLAG] if (newState.modified) return newState.copy return newState.source }
這樣,一個分割成 Store 構造函數,handler 處理對象和 produce 處理 state 這三個模塊的最簡版就完成了,將它們組合起來就是一個最最最 tiny 版的 immer ,裏面去除了不少沒必要要的校驗和冗餘的變量。但真正的 immer 內部也有其餘的功能,例如上面提到的深層嵌套對象的結構化共享等等。
固然,Proxy 做爲一個新的 API,並非全部環境都支持,Proxy 也沒法 polyfill,因此 immer 在不支持 Proxy 的環境中,使用 Object.defineProperty 來進行一個兼容。
性能
咱們用一個簡單測試來測試一下 immer 在實際中的性能。這個測試使用一個具備 100k 狀態的狀態樹,咱們記錄了一下操做 10k 個數據的時間來進行對比。
freeze 表示狀態樹在生成以後就被凍結不可繼續操做。對於普通 JS 對象,咱們可使用 Object.freeze 來凍結咱們生成的狀態樹對象,固然像 immer / immutable-js 內部本身有凍結的方法和邏輯。
具體測試文件能夠點擊查看:https://github.com/immerjs/immer/blob/master/__performance_tests__/add-data.js
這裏分別舉例一下每一個橫座標所表明的含義:
經過上圖的觀察,基本能夠得出如下對比結果:
總結
從上面的例子中咱們也能夠總結一下對比 immutable-js 和 immer 之間的優勢和不足:
字節跳動商業化前端團隊招人啦!能夠每日與技術網紅暢談理想、參加技術大牛交流分享、享受一日四餐、頂級極客裝備、免費健身房,與優秀的人作有挑戰的事情,這種地方哪裏找?快來加入咱們吧!
簡歷投遞郵箱: sunyunchao@bytedance.com