摘要:例如維護一份在內(nèi)部,來判斷是否有變化,下面這個例子就是一個構(gòu)造函數(shù),如果將它的實例傳入對象作為第一個參數(shù),就能夠后面的處理對象中使用其中的方法上面這個構(gòu)造函數(shù)相比源代碼省略了很多判斷的部分。
博客鏈接:下一代狀態(tài)管理工具 immer 簡介及源碼解析
JS 里面的變量類型可以大致分為基本類型和引用類型。在使用過程中,引用類型經(jīng)常會產(chǎn)生一些無法意識到的副作用,所以在現(xiàn)代 JS 開發(fā)過程中,大家都有意識的寫下斷開引用的不可變數(shù)據(jù)類型。
// 引用帶來的副作用 var a = [{ val: 1 }] var b = a.map(item => item.val = 2) // 期望:b 的每一個元素的 val 值變?yōu)?2 console.log(a[0].val) // 2
從上述例子我們可以發(fā)現(xiàn),本意是只想讓 b 中的每一個元素的值變?yōu)?2 ,但卻無意中改掉了 a 中每一個元素的結(jié)果,這是不符合預(yù)期的。接下來如果某個地方使用到了 a ,很容易發(fā)生一些我們難以預(yù)料并且難以 debug 的 bug。
在有了這樣的問題之后,一般來說當需要傳遞一個對象進一個函數(shù)時,我們可以使用 Object.assign 或者 ... 對對象進行解構(gòu),成功斷掉一層的引用。
例如上面的問題我們可以改用下面的這種寫法:
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 還是 ... 的解構(gòu)操作,斷掉的引用也只是一層,如果對象嵌套超過一層,這樣做還是有一定的風險。
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
這樣一來,后面的代碼如果一不小心在一個函數(shù)內(nèi)部給 b.desc 對象里面的內(nèi)容通過“點”進行賦值,就一定會改變具有相同引用的 a.desc 部分的值,這當然是不符合我們的預(yù)期的。
所以在這之后,大多數(shù)情況下我們會考慮 深拷貝 這樣的操作來完全避免上面遇到的所有問題。深拷貝,顧名思義就是在遍歷過程中,如果遇到了可能出現(xiàn)引用的數(shù)據(jù)類型,就會遞歸的完全創(chuàng)建一個新的類型。
// 一個簡單的深拷貝函數(shù),去掉了一些膠水部分 // 用戶態(tài)輸入一定是一個 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 函數(shù)進行簡單測試
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 可以滿足簡單的需求,但是真正在生產(chǎn)工作中,我們需要考慮非常多的因素。舉例來說:
key 里面 getter,setter 以及原型鏈上的內(nèi)容如何處理
value 是一個 Symbol 如何處理
value 是其他非 Plain Object 如何處理
value 內(nèi)部出現(xiàn)了一些循環(huán)引用如何處理
因為有太多不確定因素,所以我還是推薦使用大型開源項目里面的工具函數(shù),比較常用的為大家所熟知的就是 lodash.cloneDeep,無論是安全性還是效果都有所保障。
其實,這樣的概念我們常稱作 immutable ,意為不可變的數(shù)據(jù),其實理解為不可變關(guān)系更為恰當。每當我們創(chuàng)建一個被 deepClone 過的數(shù)據(jù),新的數(shù)據(jù)進行有副作用 (side effect) 的操作都不會影響到之前的數(shù)據(jù),這也就是 immutable 的精髓和本質(zhì)。
然而 deepClone 這種函數(shù)雖然斷絕了引用關(guān)系實現(xiàn)了 immutable,但是開銷實在太大。所以在 2014 年,facebook 的 immutable-js 橫空出世,即保證了 immutable ,又兼顧了性能。
immutable-js 簡介immutable-js 使用了另一套數(shù)據(jù)結(jié)構(gòu)的 API ,與我們的常見操作有些許不同,它將所有的原生對象都會轉(zhuǎn)化成 immutable-js 的內(nèi)部對象,并且任何操作最終都會返回一個新的 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 也有它的優(yōu)勢,舉個簡單的例子:
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 的數(shù)據(jù)結(jié)構(gòu)中,深層次的對象在沒有修改的情況下仍然能夠保證嚴格相等。這里的嚴格相等就可以認為是沒有新建這個對象,仍然在內(nèi)部保持著之前的引用,但是修改卻不會同步的修改。
經(jīng)常使用 React 的同學肯定也對 immutable-js 不陌生,這也就是為什么 immutable-js 會極大提高 React 頁面性能的原因之一了。
當然能夠達到 immutable 效果的當然不只這幾個個例,這篇文章我主要想介紹實現(xiàn) immutable 的庫其實是 immer。
immer 簡介immer 的作者同時也是 mobx 的作者,一個看起來非常感性的中年大叔。mobx 又像是把 Vue 的一套東西融合進了 React,已經(jīng)在社區(qū)取得了不錯的反響。immer 則是他在 immutable 方面所做的另一個實踐,在 2018-02-01,immer 成功發(fā)布了 1.0.0 版本,我差不多在一個月前開始關(guān)注這個項目,所以大清早看到作者在 twitter 上發(fā)的通告,有感而發(fā)今天寫下這篇文章,算是簡單介紹一下 immer 這個 immutable 框架的使用以及內(nèi)部簡單的實現(xiàn)原理。
與 immutable-js 最大的不同,immer 是使用原生數(shù)據(jù)結(jié)構(gòu)的 API 而不是內(nèi)置的 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 的第二個參數(shù)的函數(shù)內(nèi)部,即使給對象內(nèi)的元素直接賦值,也不會對原對象產(chǎn)生任何影響。
簡單介紹完使用之后,下面就開始簡單介紹它的內(nèi)部實現(xiàn)。不過在這之前,想先通過上面的例子簡單的發(fā)散思考一下。
通過文章最開始的例子我們就能明白,給函數(shù)傳入一個對象,直接通過“點”操作符對里面的一個屬性進行更改是一定會改變外面的結(jié)果的。而上面的這個例子中,draft 參數(shù)穿入進去,與 state 一樣也有 done 這個屬性,但是在通過 draft.done 改變值之后,原來的 state.done 并沒有發(fā)生改變。其實到這里,結(jié)合之前研究 vue 源碼的經(jīng)驗,我當時就篤定,這里一定用了 Object.defineProperty,draft 通過“點”操作的之后,一些數(shù)據(jù)的結(jié)果被劫持了,然后做了一些新的操作。
immer 原理解析真正翻開源碼,誠然里面確實有 defineProperty 的身影,不過在另一個標準的文件中,用了一種新的方式,那就是 ES6 中新增的 Proxy 對象。而在日常的業(yè)務(wù)過程中,應(yīng)該很少有前端工程師會用到 Proxy 對象,因為它的應(yīng)用場景確實有些狹隘,所以這里簡單介紹一下 Proxy 對象的使用。
Proxy 對象接受兩個參數(shù),第一個參數(shù)是需要操作的對象,第二個參數(shù)是設(shè)置對應(yīng)攔截的屬性,這里的屬性同樣也支持 get,set 等等,也就是劫持了對應(yīng)元素的讀和寫,能夠在其中進行一些操作,最終返回一個 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"
上面這個例子中傳入的第一個參數(shù)是一個空對象,當然我們可以用其他對象有內(nèi)容的對象代替它。例如維護一份 state 在內(nèi)部,來判斷是否有變化,下面這個例子就是一個構(gòu)造函數(shù),如果將它的實例傳入 Proxy 對象作為第一個參數(shù),就能夠后面的處理對象中使用其中的方法:
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 } } }
上面這個構(gòu)造函數(shù)相比源代碼省略了很多判斷的部分。實例上面有 modified,source,copy 三個屬性,有 get,set,modifing 三個方法。modified 作為內(nèi)置的 flag,判斷如何進行設(shè)置和返回。
里面最關(guān)鍵的就應(yīng)該是 modifing 這個函數(shù),如果觸發(fā)了 setter 并且之前沒有改動過的話,就會手動將 modified 這個 flag 設(shè)置為 true,并且手動通過原生的 API 實現(xiàn)一層 immutable。
對于 Proxy 的第二個參數(shù),就更加簡單了。在這個例子中,只是簡單做一層轉(zhuǎn)發(fā),任何對元素的讀取和寫入都轉(zhuǎn)發(fā)到前面的實例內(nèi)部方法去。
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 函數(shù),創(chuàng)建 store 實例后創(chuàng)建 proxy 實例。然后將創(chuàng)建的 proxy 實例傳入第二個函數(shù)中去。這樣無論在內(nèi)部做怎樣有副作用的事情,最終都會在 store 實例內(nèi)部將它解決。最終得到了修改之后的 proxy 對象,而 proxy 對象內(nèi)部已經(jīng)維護了兩份 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 構(gòu)造函數(shù),handler 處理對象和 produce 處理 state 這三個模塊的最簡版就完成了,將它們組合起來就是一個最最最 tiny 版的 immer ,里面去除了很多不必要的校驗和冗余的變量。但真正的 immer 內(nèi)部也有其他的功能,例如深度克隆情況下的結(jié)構(gòu)共享等等。
性能性能方面,就用 immer 官方 README 里面的介紹來說明情況。
這是一個關(guān)于 immer 性能的簡單測試。這個測試使用了 100000 個組件元素,并且更新其中的 10000 個。freeze 表示狀態(tài)樹在生成之后已被凍結(jié)。這是一個最佳的開發(fā)實踐,因為它可以防止開發(fā)人員意外修改狀態(tài)樹。
通過上圖的觀察,基本可以得出:
從 immer 的角度來看,這個性能環(huán)境比其他框架和庫要惡劣的多,因為它必須代理的根節(jié)點相對于其余的數(shù)據(jù)集來說大得多
從 mutate 和 deepclone 來看,mutate 基準確定了數(shù)據(jù)更改費用的基線,沒有不可變性(或深度克隆情況下的結(jié)構(gòu)共享)
使用 Proxy 的 immer 大概是手寫 reducer 的兩倍,當然這在實踐中可以忽略不計
immer 大致和 immutable-js 一樣快。但是,immutable-js 最后經(jīng)常需要 toJS 操作,這里的性能的開銷是很大的。例如將不可變的 JS 對象轉(zhuǎn)換回普通的對象,將它們傳遞給組件中,或著通過網(wǎng)絡(luò)傳輸?shù)鹊龋ㄟ€有將從例如服務(wù)器接收到的數(shù)據(jù)轉(zhuǎn)換為 immutable-js 內(nèi)置對象的前期成本)
immer 的 ES5 實現(xiàn)速度明顯較慢。對于大多數(shù)的 reducer 來說,這并不重要,因為處理大量數(shù)據(jù)的 reducer 可以完全不(或者僅部分)使用 immer 的 produce 函數(shù)。幸運的是,immer 完全支持這種選擇性加入的情況
在 freeze 的版本中,只有 mutate,deepclone 和原生 reducer 才能夠遞歸地凍結(jié)全狀態(tài)樹,而其他測試用例只凍結(jié)樹的修改部分
寫在后面其實縱觀 immer 的實現(xiàn),核心的原理就是放在了對對象讀寫的劫持,從表現(xiàn)形式上立刻就能讓人想到 vue ,mobx 從核心原理上來說也是對對象的讀寫劫持,最近有另一篇非?;鸬奈恼?-- 如何讓 (a == 1 && a == 2 && a == 3) 為 true,也相信不少的小伙伴讀過,除了那個肉眼不可見字符的答案,其他答案也算是對對象的讀寫劫持從而達到目標。
所以說在 JS 中,很多知識相輔相成,有多少種方式能讓 (a == 1 && a == 2 && a == 3) 為 true,理論上有多少種答案就會有多少種 MVVM 的組成方式,甚至就有多少種方法能夠?qū)崿F(xiàn)這樣的 immutable。所以任何一點點小的知識點的聚合,未來都可能影響前端的發(fā)展。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/107140.html
摘要:所以整個過程只涉及三個輸入狀態(tài),中間狀態(tài),輸出狀態(tài)關(guān)鍵是是如何生成,如何應(yīng)用修改,如何生成最終的。至此基本把上的模式解析完畢。結(jié)束實現(xiàn)還是相當巧妙的,以后可以在狀態(tài)管理上使用一下。 開始 在函數(shù)式編程中,Immutable這個特性是相當重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發(fā)者用上這樣的特性,所...
摘要:所以整個過程只涉及三個輸入狀態(tài),中間狀態(tài),輸出狀態(tài)關(guān)鍵是是如何生成,如何應(yīng)用修改,如何生成最終的。至此基本把上的模式解析完畢。結(jié)束實現(xiàn)還是相當巧妙的,以后可以在狀態(tài)管理上使用一下。 開始 在函數(shù)式編程中,Immutable這個特性是相當重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發(fā)者用上這樣的特性,所...
摘要:無奈網(wǎng)絡(luò)上完善的文檔實在太少,所以自己寫了一份,本篇文章以貼近實戰(zhàn)的思路和流程,對進行了全面的講解。這使得成為了真正的不可變數(shù)據(jù)。的使用非常靈活,多多思考,相信你還可以發(fā)現(xiàn)更多其他的妙用參考文檔官方文檔 文章在 github 開源, 歡迎 Fork 、Star 前言 Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現(xiàn)是利用 ES6 的 proxy,幾乎以最小的成...
閱讀 3124·2021-11-24 09:39
閱讀 986·2021-09-07 10:20
閱讀 2406·2021-08-23 09:45
閱讀 2282·2021-08-05 10:00
閱讀 582·2019-08-29 16:36
閱讀 846·2019-08-29 11:12
閱讀 2831·2019-08-26 11:34
閱讀 1848·2019-08-26 10:56