博客連接: 下一代狀態管理工具 immer 簡介及源碼解析
JS 裏面的變量類型能夠大體分爲基本類型和引用類型。在使用過程當中,引用類型常常會產生一些沒法意識到的反作用,因此在現代 JS 開發過程當中,你們都有意識的寫下斷開引用的不可變數據類型。前端
// 引用帶來的反作用 var a = [{ val: 1 }] var b = a.map(item => item.val = 2) // 指望:b 的每個元素的 val 值變爲 2 console.log(a[0].val) // 2
從上述例子咱們能夠發現,本意是隻想讓 b 中的每個元素的值變爲 2 ,但卻無心中改掉了 a 中每個元素的結果,這是不符合預期的。接下來若是某個地方使用到了 a ,很容易發生一些咱們難以預料而且難以 debug 的 bug。vue
在有了這樣的問題以後,通常來講當須要傳遞一個對象進一個函數時,咱們可使用 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
仍是 ...
的解構操做,斷掉的引用也只是一層,若是對象嵌套超過一層,這樣作仍是有必定的風險。網絡
var a = [ { val: 1, desc: { text: 'a' } } ] var b = a.map(item => ({ ...item, val: 2 })) console.log(a === b) // false console.log(a.desc === b.desc) // true
這樣一來,後面的代碼若是一不當心在一個函數內部給 b.desc
對象裏面的內容經過「點」進行賦值,就必定會改變具備相同引用的 a.desc
部分的值,這固然是不符合咱們的預期的。前端工程師
因此在這以後,大多數狀況下咱們會考慮 深拷貝 這樣的操做來徹底避免上面遇到的全部問題。深拷貝,顧名思義就是在遍歷過程當中,若是遇到了可能出現引用的數據類型,就會遞歸的徹底建立一個新的類型。數據結構
// 一個簡單的深拷貝函數,去掉了一些膠水部分 // 用戶態輸入必定是一個 Plain Object,而且全部 value 也是 Plain Object function deepClone(a) { const keys = Object.keys(a) return keys.reduce((memo, current) => { const value = a[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
能夠知足簡單的需求,可是真正在生產工做中,咱們須要考慮很是多的因素。舉例來講:ide
由於有太多不肯定因素,因此我仍是推薦使用大型開源項目裏面的工具函數,比較經常使用的爲你們所熟知的就是 lodash.cloneDeep
,不管是安全性仍是效果都有所保障。函數
其實,這樣的概念咱們常稱做 immutable ,意爲不可變的數據,其實理解爲不可變關係更爲恰當。每當咱們建立一個被 deepClone
過的數據,新的數據進行有反作用 (side effect) 的操做都不會影響到以前的數據,這也就是 immutable 的精髓和本質。
然而 deepClone 這種函數雖然斷絕了引用關係實現了 immutable,可是開銷實在太大。因此在 2014 年,facebook 的 immutable-js 橫空出世,即保證了 immutable ,又兼顧了性能。
immutable-js 使用了另外一套數據結構的 API ,與咱們的常見操做有些許不一樣,它將全部的原生對象都會轉化成 immutable-js 的內部對象,而且任何操做最終都會返回一個新的 immutable 的值。
上面的例子使用 immutable-js 就須要這樣改造一下:
const { fromJS } = require('immutable') const data = { val: 1, desc: { text: 'a', }, } const a = fromJS(data) 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', }, } const a = fromJS(data) const b = a.setIn(['desc', 'text'], 'b') console.log(b.get('desc') === a.get('desc')) // false console.log(b.get('content') === a.get('content')) // true const c = a.toJS() const d = b.toJS() console.log(c.desc === d.desc) // false console.log(c.content === d.content) // false
從上面的例子能夠看出來,在 immutable-js 的數據結構中,深層次的對象在沒有修改的狀況下仍然可以保證嚴格相等。這裏的嚴格相等就能夠認爲是沒有新建這個對象,仍然在內部保持着以前的引用,可是修改卻不會同步的修改。
常用 React 的同窗確定也對 immutable-js 不陌生,這也就是爲何 immutable-js 會極大提升 React 頁面性能的緣由之一了。
固然可以達到 immutable 效果的固然不僅這幾個個例,這篇文章我主要想介紹實現 immutable 的庫實際上是 immer。
immer 的做者同時也是 mobx 的做者,一個看起來很是感性的中年大叔。mobx 又像是把 Vue 的一套東西融合進了 React,已經在社區取得了不錯的反響。immer 則是他在 immutable 方面所作的另外一個實踐,在 2018-02-01,immer 成功發佈了 1.0.0 版本,我差很少在一個月前開始關注這個項目,因此大清早看到做者在 twitter 上發的通告,有感而發今天寫下這篇文章,算是簡單介紹一下 immer 這個 immutable 框架的使用以及內部簡單的實現原理。
與 immutable-js 最大的不一樣,immer 是使用原生數據結構的 API 而不是內置的 API,舉個簡單例子:
const produce = require('immer') const state = { done: false, val: 'string', } const newState = produce(state, (draft) => { draft.done = true }) console.log(state.done) // false console.log(newState.done) // true
全部須要更改的邏輯均可以放進 produce 的第二個參數的函數內部,即便給對象內的元素直接賦值,也不會對原對象產生任何影響。
簡單介紹完使用以後,下面就開始簡單介紹它的內部實現。不過在這以前,想先經過上面的例子簡單的發散思考一下。
經過文章最開始的例子咱們就能明白,給函數傳入一個對象,直接經過「點」操做符對裏面的一個屬性進行更改是必定會改變外面的結果的。而上面的這個例子中,draft
參數穿入進去,與 state
同樣也有 done 這個屬性,可是在經過 draft.done
改變值以後,原來的 state.done
並無發生改變。其實到這裏,結合以前研究 vue 源碼的經驗,我當時就篤定,這裏必定用了 Object.defineProperty
,draft 經過「點」操做的以後,一些數據的結果被劫持了,而後作了一些新的操做。
真正翻開源碼,誠然裏面確實有 defineProperty 的身影,不過在另外一個標準的文件中,用了一種新的方式,那就是 ES6 中新增的 Proxy 對象。而在平常的業務過程當中,應該不多有前端工程師會用到 Proxy 對象,由於它的應用場景確實有些狹隘,因此這裏簡單介紹一下 Proxy 對象的使用。
Proxy 對象接受兩個參數,第一個參數是須要操做的對象,第二個參數是設置對應攔截的屬性,這裏的屬性一樣也支持 get,set 等等,也就是劫持了對應元素的讀和寫,可以在其中進行一些操做,最終返回一個 Proxy 對象。
const proxy = new Proxy({}, { get(target, key) { console.log('proxy get key', key) }, set(target, key, value) { console.log('value', value) } }) proxy.info // 'proxy get key info' proxy.info = 1 // 'value 1'
上面這個例子中傳入的第一個參數是一個空對象,固然咱們能夠用其餘對象有內容的對象代替它。例如維護一份 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 this.copy = Array.isArray(this.source) ? this.source.slice() : { ...this.source } } }
上面這個構造函數相比源代碼省略了不少判斷的部分。實例上面有 modified,source,copy 三個屬性,有 get,set,modifing 三個方法。modified 做爲內置的 flag,判斷如何進行設置和返回。
裏面最關鍵的就應該是 modifing
這個函數,若是觸發了 setter 而且以前沒有改動過的話,就會手動將 modified
這個 flag 設置爲 true
,而且手動經過原生的 API 實現一層 immutable。
對於 Proxy 的第二個參數,就更加簡單了。在這個例子中,只是簡單作一層轉發,任何對元素的讀取和寫入都轉發到前面的實例內部方法去。
const PROXY_FLAG = '@@SYMBOL_PROXY_FLAG' const handler = { get(target, key) { 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) const newState = proxy[PROXY_FLAG] if (newState.modified) return newState.copy return newState.source }
這樣,一個分割成 Store 構造函數,handler 處理對象和 produce 處理 state 這三個模塊的最簡版就完成了,將它們組合起來就是一個最最最 tiny 版的 immer ,裏面去除了不少沒必要要的校驗和冗餘的變量。但真正的 immer 內部也有其餘的功能,例如深度克隆狀況下的結構共享等等。
性能方面,就用 immer 官方 README 裏面的介紹來講明狀況。
這是一個關於 immer 性能的簡單測試。這個測試使用了 100000 個組件元素,而且更新其中的 10000 個。freeze 表示狀態樹在生成以後已被凍結。這是一個最佳的開發實踐,由於它能夠防止開發人員意外修改狀態樹。
經過上圖的觀察,基本能夠得出:
其實縱觀 immer 的實現,核心的原理就是放在了對對象讀寫的劫持,從表現形式上馬上就能讓人想到 vue ,mobx 從核心原理上來講也是對對象的讀寫劫持,最近有另外一篇很是火的文章 -- 如何讓 (a == 1 && a == 2 && a == 3)
爲 true,也相信很多的小夥伴讀過,除了那個肉眼不可見字符的答案,其餘答案也算是對對象的讀寫劫持從而達到目標。
因此說在 JS 中,不少知識相輔相成,有多少種方式能讓 (a == 1 && a == 2 && a == 3) 爲 true,理論上有多少種答案就會有多少種 MVVM 的組成方式,甚至就有多少種方法可以實現這樣的 immutable。因此任何一點點小的知識點的聚合,將來均可能影響前端的發展。