我們知道 js 對(duì)象是按共享傳遞(call by sharing
)的,因此在處理復(fù)雜 js 對(duì)象的時(shí)候镇饺,往往會(huì)因?yàn)樾薷牧藢?duì)象而產(chǎn)生副作用———因?yàn)椴恢勒l(shuí)還引用著這份數(shù)據(jù)乎莉,不知道這些修改會(huì)影響到誰(shuí)。因此我們經(jīng)常會(huì)把對(duì)象做一次拷貝再放到處理函數(shù)中奸笤。最常見(jiàn)的拷貝是利用 Object.assign()
新建一個(gè)副本或者利用 ES6 的 對(duì)象解構(gòu)運(yùn)算惋啃,但它們僅僅只是淺拷貝。
深拷貝
如果需要深拷貝监右,拷貝的時(shí)候判斷一下屬性值的類(lèi)型边灭,如果是對(duì)象,再遞歸調(diào)用深拷貝函數(shù)即可健盒,具體實(shí)現(xiàn)可以參考 jQuery 的 $.extend
绒瘦。實(shí)際上需要處理的邏輯分支比較多,在 lodash 中 的深拷貝函數(shù) cloneDeep 甚至有上百行扣癣,那有沒(méi)有簡(jiǎn)單粗暴點(diǎn)的辦法呢惰帽?
JSON.parse
最原始又有效的做法便是利用 JSON.parse
將該對(duì)象轉(zhuǎn)換為其 JSON 字符串表示形式,然后將其解析回對(duì)象:
const deepClone(obj) => JSON.parse(JSON.stringify(obj));
復(fù)制代碼
對(duì)于大部分場(chǎng)景來(lái)說(shuō)父虑,除了解析字符串略耗性能外(其實(shí)真的可以忽略不計(jì))该酗,確實(shí)是個(gè)實(shí)用的方法。但是尷尬的是它不能處理循環(huán)對(duì)象(父子節(jié)點(diǎn)互相引用)的情況频轿,而且也無(wú)法處理對(duì)象中有 function、正則等情況烁焙。
MessageChannel
MessageChannel 接口是信道通信 API 的一個(gè)接口航邢,它允許我們創(chuàng)建一個(gè)新的信道并通過(guò)信道的兩個(gè) MessagePort 屬性來(lái)傳遞數(shù)據(jù)
利用這個(gè)特性,我們可以創(chuàng)建一個(gè) MessageChannel
骄蝇,向其中一個(gè) port 發(fā)送數(shù)據(jù)膳殷,另一個(gè) port 就能收到數(shù)據(jù)了。
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
const obj = /* ... */
const clone = await structuralClone(obj);
復(fù)制代碼
除了這樣的寫(xiě)法是異步的以外也沒(méi)什么大的問(wèn)題了九火,它能很好的支持循環(huán)對(duì)象赚窃、內(nèi)置對(duì)象(Date、 正則)等情況岔激,瀏覽器兼容性也還行勒极。但是它同樣也無(wú)法處理對(duì)象中有 function的情況。
類(lèi)似的 API 還有 History API
虑鼎、Notification API
等辱匿,都是利用了結(jié)構(gòu)化克隆算法(Structured Clone
) 實(shí)現(xiàn)傳輸值的键痛。
Immutable
如果需要頻繁地操作一個(gè)復(fù)雜對(duì)象,每次都完全深拷貝一次的話(huà)效率太低了匾七。大部分場(chǎng)景下都只是更新了這個(gè)對(duì)象一兩個(gè)字段絮短,其他的字段都不變,對(duì)這些不變的字段的拷貝明顯是多余的昨忆《∑担看看 Dan Abramov 大佬說(shuō)的:
這些庫(kù)的關(guān)鍵思路即是:創(chuàng)建 持久化的數(shù)據(jù)結(jié)構(gòu)
(Persistent data structure
),在操作對(duì)象的時(shí)候只 clone 變化的節(jié)點(diǎn)和其祖先節(jié)點(diǎn)邑贴,其他的保持不變席里,實(shí)現(xiàn) 結(jié)構(gòu)共享
(structural sharing
)。例如在下圖中紅色節(jié)點(diǎn)發(fā)生變化后痢缎,只會(huì)重新產(chǎn)生綠色的 3 個(gè)節(jié)點(diǎn)胁勺,其余的節(jié)點(diǎn)保持復(fù)用(類(lèi)似軟鏈的感覺(jué))。這樣就由原本深拷貝需要?jiǎng)?chuàng)建的 8 個(gè)新節(jié)點(diǎn)減少到只需要 3 個(gè)新節(jié)點(diǎn)了独旷。
Immutable.js
在 Immutable.js
中這里的 “節(jié)點(diǎn)” 并不能簡(jiǎn)單理解成對(duì)象中的 “key”署穗,其內(nèi)部使用了 Trie(字典樹(shù))
數(shù)據(jù)結(jié)構(gòu), Immutable.js
會(huì)把對(duì)象所有的 key 進(jìn)行 hash 映射,將得到的 hash 值轉(zhuǎn)化為二進(jìn)制嵌洼,從后向前每 5 位進(jìn)行分割后再轉(zhuǎn)化為 Trie 樹(shù)案疲。
舉個(gè)例子,假如有一對(duì)象 zoo:
zoo={
'frog':??
'panda':??,
'monkey':??,
'rabbit':??,
'tiger':??,
'dog':{
'dog1':??,
'dog2':??,
...// 還有 100 萬(wàn)只 dog
}
...// 剩余還有 100 萬(wàn)個(gè)的字段
}
復(fù)制代碼
'frog'進(jìn)行 hash 之后的值為 3151780麻养,轉(zhuǎn)成二進(jìn)制 11 00000 00101 11101 00100
褐啡,同理'dog' hash 后轉(zhuǎn)二機(jī)制為 11 00001 01001 11100
那么 frog 和 dog 在 immutable 對(duì)象的 Trie 樹(shù)的位置分別是:
當(dāng)然實(shí)際的 Trie 樹(shù)會(huì)根據(jù)實(shí)際對(duì)象進(jìn)行剪枝處理,沒(méi)有值的分支會(huì)被剪掉鳖昌,不會(huì)每個(gè)節(jié)點(diǎn)都長(zhǎng)滿(mǎn)了 32 個(gè)子節(jié)點(diǎn)备畦。
比如某天需要將 zoo.frog 由 ?? 改成 ?? ,發(fā)生變動(dòng)的節(jié)點(diǎn)只有上圖中綠色的幾個(gè)许昨,其他的節(jié)點(diǎn)直接復(fù)用懂盐,這樣比深拷貝產(chǎn)生 100 萬(wàn)個(gè)節(jié)點(diǎn)效率高了很多。
總的來(lái)說(shuō)糕档,使用 Immutable.js 在處理大量數(shù)據(jù)的情況下和直接深拷貝相比效率高了不少莉恼,但對(duì)于一般小對(duì)象來(lái)說(shuō)其實(shí)差別不大。不過(guò)如果需要改變一個(gè)嵌套很深的對(duì)象, Immutable.js 倒是比直接 Object.assign 或者解構(gòu)的寫(xiě)法上要簡(jiǎn)潔些速那。
例如修改 zoo.dog.dog1.name.firstName = 'haha'
,兩種寫(xiě)法分別是:
// 對(duì)象解構(gòu)
const zoo2 = {...zoo,dog:{...zoo.dog,dog1:{...zoo.dog.dog1,name:{...zoo.dog.dog1,firstName:'haha'}}}}
//Immutable.js 這里的 zoo 是 Immutable 對(duì)象
const zoo2 = zoo.updateIn(['dog','dog1','name','firstName'],(oldValue)=>'haha')
復(fù)制代碼
seamless-immutable
如果數(shù)據(jù)量不大但想用這種類(lèi)似 updateIn
便利的語(yǔ)法的話(huà)可以用 seamless-immutable
俐银。這個(gè)庫(kù)就沒(méi)有上面的 Trie 這些幺蛾子了,就是為其擴(kuò)展了 updateIn
端仰、merge
等 9 個(gè)方法的普通簡(jiǎn)單對(duì)象捶惜,利用 Object.freeze
凍結(jié)對(duì)象本身改動(dòng), 每次修改返回副本。感覺(jué)像是閹割版荔烧,性能不及 Immutable.js售躁,但在部分場(chǎng)景下也是適用的坞淮。
類(lèi)似的庫(kù)還有 Dan Abramov 大佬提到的 immutability-helper
和 updeep
,它們的用法和實(shí)現(xiàn)都比較類(lèi)似陪捷,其中諸如 updateIn
的方法分別是通過(guò) Object.assign
和對(duì)象解構(gòu)實(shí)現(xiàn)的回窘。
Immer.js
而 Immer.js 的寫(xiě)法可以說(shuō)是一股清流了:
import produce from "immer"
const zoo2 = produce(zoo, draft=>{
draft.dog.dog1.name.firstName = 'haha'
})
復(fù)制代碼
雖然遠(yuǎn)看不是很優(yōu)雅,但是寫(xiě)起來(lái)倒比較簡(jiǎn)單市袖,所有需要更改的邏輯都可以放進(jìn) produce
的第二個(gè)參數(shù)的函數(shù)(稱(chēng)為 producer 函數(shù)
)內(nèi)部啡直,不會(huì)對(duì)原對(duì)象造成任何影響。在 producer 函數(shù)內(nèi)可以同時(shí)更改多個(gè)字段苍碟,一次性操作酒觅,非常方便。
這種用 “點(diǎn)” 操作符類(lèi)似原生操作的方法很明顯是劫持了數(shù)據(jù)結(jié)果然后做新的操作∥⒎澹現(xiàn)在很多框架也喜歡這么搞舷丹,用 Object.defineProperty
達(dá)到效果。而 Immer.js 卻是用的 Proxy
實(shí)現(xiàn)的:對(duì)原始數(shù)據(jù)中每個(gè)訪(fǎng)問(wèn)到的節(jié)點(diǎn)都創(chuàng)建一個(gè) Proxy蜓肆,修改節(jié)點(diǎn)時(shí)修改副本而不操作原數(shù)據(jù)颜凯,最后返回到對(duì)象由未修改的部分和已修改的副本組成。
在 immer.js 中每個(gè)代理的對(duì)象的結(jié)構(gòu)如下:
function createState(parent, base) {
return {
modified: false, // 是否被修改過(guò),
assigned:{},// 記錄哪些 key 被改過(guò)或者刪除,
finalized: false // 是否完成
base, // 原數(shù)據(jù)
parent, // 父節(jié)點(diǎn)
copy: undefined, // base 和 proxies 屬性的淺拷貝
proxies: {}, // 記錄哪些 key 被代理了
}
}
復(fù)制代碼
在調(diào)用原對(duì)象的某 key 的 getter 的時(shí)候仗扬,如果這個(gè) key 已經(jīng)被改過(guò)了則返回 copy 中的對(duì)應(yīng) key 的值症概,如果沒(méi)有改過(guò)就為這個(gè)子節(jié)點(diǎn)創(chuàng)建一個(gè)代理再直接返回原值。 調(diào)用某 key 的 setter 的時(shí)候早芭,就直接改 copy 里的值彼城。如果是第一次修改,還需要先把 base 的屬性和 proxies 的上的屬性都淺拷貝給 copy退个。同時(shí)還根據(jù) parent 屬性遞歸父節(jié)點(diǎn)募壕,不斷淺拷貝,直到根節(jié)點(diǎn)為止语盈。
仍然以 draft.dog.dog1.name.firstName = 'haha'
為例舱馅,會(huì)依次觸發(fā) dog、dog1黎烈、name 節(jié)點(diǎn)的 getter习柠,生成 proxy匀谣。對(duì) name 節(jié)點(diǎn)的 firstName 執(zhí)行 setter 操作時(shí)會(huì)先將 name 所有屬性淺拷貝至節(jié)點(diǎn)的 copy 屬性再直接修改 copy照棋,然后將 name 節(jié)點(diǎn)的所有父節(jié)點(diǎn)也依次淺拷貝到自己的 copy 屬性。當(dāng)所有修改結(jié)束后會(huì)遍歷整個(gè)樹(shù)武翎,返回新的對(duì)象包括每個(gè)節(jié)點(diǎn)的 base 沒(méi)有修改的部分和其在 copy 中被修改的部分烈炭。
總結(jié)
操作大量數(shù)據(jù)的情況下 Immutable.js 是個(gè)不錯(cuò)的選擇。一般數(shù)據(jù)量不大的情況下宝恶,對(duì)于嵌套較深的對(duì)象用 immer 或者 seamless-immutable 都不錯(cuò)符隙,看個(gè)人習(xí)慣哪種寫(xiě)法了趴捅。如果想要 “完美” 的深拷貝,就得用 lodash 了??霹疫。
擴(kuò)展閱讀
作者:表示很不蛋定
鏈接:https://juejin.im/post/5bbad07ce51d450e894e4228
來(lái)源:掘金
著作權(quán)歸作者所有拱绑。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處丽蝎。