精讀《Immer.js》源碼

本週精讀的倉庫是 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》源碼》

相關文章
相關標籤/搜索