本週精讀的倉庫是 immer。
1 引言 Immer 是最近火起來的一個項目,由 Mobx 做者 Mweststrate 研發。
瞭解 mobx 的同窗可能會發現,Immer 就是更底層的 Mobx,它將 Mobx 特性發揚光大,得以結合到任何數據流框架,使用起來很是優雅。
2 概述 麻煩的 Immutable Immer 想解決的問題,是利用元編程簡化 Immutable 使用的複雜度。舉個例子,咱們寫一個純函數:php
const addProducts = products => { const cloneProducts = products.slice() cloneProducts.push({ text: "shoes" }) return cloneProducts }
雖然代碼並不複雜,但寫起來心裏仍隱隱做痛。咱們必須將前端
products
拷貝一份,再調用react
push
函數修改新的編程
cloneProducts
,再返回它。
若是 js 原生支持 Immutable,就能夠直接使用redux
push
了!對,Immer 讓 js 如今就支持:性能優化
const addProducts = produce(products => { products.push({ text: "shoes" }) })
頗有趣吧,這兩個框架
addProducts
函數功能一摸同樣,並且都是純函數。
彆扭的 setState 咱們都知道,react 框架中,異步
setState
支持函數式寫法:mvvm
this.setState(state => ({ ...state, isShow: true }))
配合解構語法,寫起來還是如此優雅。那數據稍微複雜些呢?咱們就要默默忍受 「糟糕的 Immutable」 了:函數
this.setState(state => { const cloneProducts = state.products.slice() cloneProducts.push({ text: "shoes" }) return { ...state, cloneProducts } })
然而有了 Immer,一切都不同了:
this.setState(produce(state => (state.isShow = true))) this.setState(produce(state => state.products.push({ text: "shoes" })))
方便的柯里化 上面講述了 Immer 支持柯里化帶來的好處。因此咱們也能夠直接把兩個參數一次性消費:
const oldObj = { value: 1 } const newObj = produce(oldObj, draft => (draft.value = 2))
這就是 Immer:Create the next immutable state by mutating the current one.
3 精讀 雖然筆者以前在這方面已經有所研究,好比作出了 Mutable 轉 Immutable 的庫:dob-redux,但 Immer 實在是太驚豔了,Immer 是更底層的拼圖,它能夠插入到任何數據流框架做爲功能加強,不得不讚嘆 Mweststrate 真的是很是高瞻遠矚。
因此筆者認真閱讀了它的源代碼,帶你們從原理角度認識 Immer。
Immer 是一個支持柯里化,僅支持同步計算的工具,因此很是適合做爲 redux 的 reducer 使用。
Immer 也支持直接 return value,這個功能比較簡單,因此本篇會跳過全部對 return value 的處理。PS: mutable 與 return 不能同時返回不一樣對象,不然弄不清楚到哪一種修改是有效的。
柯里化這裏不作拓展介紹,詳情查看 curry。咱們看
produce
函數 callback 部分:
produce(obj, draft => { draft.count++ })
obj
是個普通對象,那黑魔法必定出如今
draft
對象上,Immer 給
draft
對象的全部屬性作了監聽。
因此總體思路就有了:
draft
是
obj
的代理,對
draft
mutable 的修改都會流入到自定義
setter
函數,它並不修改原始對象的值,而是遞歸父級不斷淺拷貝,最終返回新的頂層對象,做爲
produce
函數的返回值。
生成代理 第一步,也就是將
obj
轉爲
draft
這一步,爲了提升 Immutable 運行效率,咱們須要一些額外信息,所以將
obj
封裝成一個包含額外信息的代理對象:
{ modified, // 是否被修改過 finalized, // 是否已經完成(全部 setter 執行完,而且已經生成了 copy) parent, // 父級對象 base, // 原始對象(也就是 obj) copy, // base(也就是 obj)的淺拷貝,使用 Object.assign(Object.create(null), obj) 實現 proxies, // 存儲每一個 propertyKey 的代理對象,採用懶初始化策略 }
在這個代理對象上,綁定了自定義的
getter
setter
,而後直接將其扔給
produce
執行。
getter
produce
回調函數中包含了用戶的
mutable
代碼。因此如今入口變成了
getter
與
setter
。
getter
主要用來懶初始化代理對象,也就是當代理對象子屬性被訪問的時候,纔會生成其代理對象。
這麼說比較抽象,舉個例子,下面是原始 obj:
{ a: {}, b: {}, c: {} }
那麼初始狀況下,
draft
是
obj
的代理,因此訪問
draft.a
draft.b
draft.c
時,都能觸發
getter
setter
,進入自定義處理邏輯。但是對
draft.a.x
就沒法監聽了,由於代理只能監聽一層。
代理懶初始化就是要解決這個問題,當訪問到
draft.a
時,自定義
getter
已經悄悄生成了新的針對
draft.a
對象的代理
draftA
,所以
draft.a.x
至關於訪問了
draftA.x
,因此能遞歸監聽一個對象的全部屬性。
同時,若是代碼中只訪問了
draft.a
,那麼只會在內存生成
draftA
代理,
b
c
屬性由於沒有訪問,所以不須要浪費資源生成代理
draftB
draftC
。
固然 Immer 作了一些性能優化,以及在對象被修改過(
modified
)獲取其
copy
對象,爲了保證
base
是不可變的,這裏不作展開。
setter 當對
draft
修改時,會對
base
也就是原始值進行淺拷貝,保存到
copy
屬性,同時將
modified
屬性設置爲
true
。這樣就完成了最重要的 Immutable 過程,並且淺拷貝並非很消耗性能,加上是按需淺拷貝,所以 Immer 的性能還能夠。
同時爲了保證整條鏈路的對象都是新對象,會根據
parent
屬性遞歸父級,不斷淺拷貝,直到這個葉子結點到根結點整條鏈路對象都換新爲止。
完成了
modified
對象再有屬性被修改時,會將這個新值保存在
copy
對象上。
生成 Immutable 對象 當執行完
produce
後,用戶的全部修改已經完成(因此 Immer 沒有支持異步),若是
modified
屬性爲
false
,說明用戶根本沒有改這個對象,那直接返回原始
base
屬性便可。
若是
modified
屬性爲
true
,說明對象發生了修改,返回
copy
屬性便可。可是
setter
過程是遞歸的,
draft
的子對象也是
draft
(包含了
base
copy
modified
等額外屬性的代理),咱們必須一層層遞歸,拿到真正的值。
因此在這個階段,全部
draft
的
finalized
都是
false
,
copy
內部可能還存在大量
draft
屬性,所以遞歸
base
與
copy
的子屬性,若是相同,就直接返回;若是不一樣,遞歸一次整個過程(從這小節第一行開始)。
最後返回的對象是由
base
的一些屬性(沒有修改的部分)和
copy
的一些屬性(修改的部分)最終拼接而成的。最後使用
freeze
凍結
copy
屬性,將
finalized
屬性設置爲
true
。
至此,返回值生成完畢,咱們將最終值保存在
copy
屬性上,並將其凍結,返回了 Immutable 的值。
Immer 所以完成了難以想象的操做:Create the next immutable state by mutating the current one。
源碼讀到這裏,發現 Immer 其實能夠支持異步,只要支持 produce 函數返回 Promise 便可。最大的問題是,最後對代理的
revoke
清洗,須要藉助全局變量,這一點阻礙了 Immer 對異步的支持。
4 總結 讀到這,若是以爲不過癮,能夠看看 redux-box 這個庫,利用 immer + redux 解決了 reducer 冗餘
return
的問題。
一樣咱們也開始思考並設計新的數據流框架,筆者在 2018.3.24 的攜程技術沙龍將會分享 《mvvm 前端數據流框架精講》,分享這幾年涌現的各套數據流技術方案研究心得,感興趣的同窗歡迎報名參加。
5 更多討論 討論地址是:精讀《Immer.js》源碼》 · Issue #68 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,每週五發布。
轉載於猿2048:➻《精讀《Immer.js》源碼》