克隆:深拷貝田度、Immutable 妒御、seamless-immutable & Immer

原文

我們知道 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ō)的:

image.png

這些庫(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)了独旷。

image.png

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ù)的位置分別是:

image.png
image.png

當(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)效率高了很多。

image.png

總的來(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-helperupdeep,它們的用法和實(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)為止语盈。

image.png

仍然以 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ò)展閱讀

  1. Deep-copying in JavaScript
  2. Introducing Immer: Immutability the easy way

作者:表示很不蛋定
鏈接:https://juejin.im/post/5bbad07ce51d450e894e4228
來(lái)源:掘金
著作權(quán)歸作者所有拱绑。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處丽蝎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猎拨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屠阻,更是在濱河造成了極大的恐慌红省,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件国觉,死亡現(xiàn)場(chǎng)離奇詭異吧恃,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)麻诀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)痕寓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人针饥,你說(shuō)我怎么就攤上這事厂抽。” “怎么了丁眼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵筷凤,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我苞七,道長(zhǎng)藐守,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任蹂风,我火速辦了婚禮卢厂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惠啄。我一直安慰自己慎恒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布撵渡。 她就那樣靜靜地躺著融柬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪趋距。 梳的紋絲不亂的頭發(fā)上粒氧,一...
    開(kāi)封第一講書(shū)人閱讀 49,985評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音节腐,去河邊找鬼外盯。 笑死摘盆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饱苟。 我是一名探鬼主播孩擂,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼箱熬!你這毒婦竟也來(lái)了肋殴?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤坦弟,失蹤者是張志新(化名)和其女友劉穎护锤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體酿傍,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烙懦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赤炒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氯析。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖莺褒,靈堂內(nèi)的尸體忽然破棺而出掩缓,到底是詐尸還是另有隱情,我是刑警寧澤遵岩,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布你辣,位于F島的核電站,受9級(jí)特大地震影響尘执,放射性物質(zhì)發(fā)生泄漏舍哄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一誊锭、第九天 我趴在偏房一處隱蔽的房頂上張望表悬。 院中可真熱鬧,春花似錦丧靡、人聲如沸蟆沫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饭庞。三九已至,卻和暖如春罐盔,著一層夾襖步出監(jiān)牢的瞬間但绕,已是汗流浹背救崔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工惶看, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捏顺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓纬黎,卻偏偏與公主長(zhǎng)得像幅骄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子本今,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容