文章首發自 Githubgit
JS 中有個重要的類型叫作引用類型。這種類型在使用的過程當中,由於傳遞的值是引用,因此很容易發生一些反作用,好比:github
let a = { age: 1 }
let b = a
b.age = 2
複製代碼
上述代碼的寫法會形成 a
和 b
的屬性都被修改了。你們在平常開發中確定不想出現這種狀況,因此都會用上一些手段去斷開它們的引用鏈接。對於上述的數據結構來講,淺拷貝就能解決咱們的問題。數組
let b = { ...a }
b.age = 2
複製代碼
可是淺拷貝只能斷開一層的引用,若是數據結構是多層對象的話,淺拷貝就不能解決問題了,這時候咱們須要用到深拷貝。數據結構
深拷貝的作法通常分兩種:函數
JSON.parse(JSON.stringify(a))
第一種作法存在一些侷限,不少狀況下並不能使用,所以這裏就不提了;第二種作法通常是工具庫中的深拷貝函數實現方式,好比 loadash 中的 cloneDeep
。雖然這種作法能解決第一種作法的侷限,可是對於龐大的數據來講性能並很差,由於須要把整個對象都遍歷一遍。工具
那麼是否能夠有一種實現的作法,只有當屬性修改之後纔對這部分數據作深拷貝,又能解決 JSON.parse(JSON.stringify(a))
的侷限呢。這種作法固然是存在的,惟一的點是咱們如何知道用戶修改了什麼屬性?性能
答案是 Proxy
,經過攔截 set
和 get
就能達到咱們想要的,固然 Object.defineProperty()
也能夠。其實 Immer 這個庫就是用了這種作法來生成不可變對象的,接下來就讓咱們來試着經過 Proxy
來實現高性能版的深拷貝。ui
先說下總體核心思路,其實就三點:spa
set
,全部賦值都在 copy (原數據淺拷貝的對象)中進行,這樣就不會影響到原對象get
,經過屬性是否修改的邏輯分別從 copy 或者原數據中取值接下來是實現,咱們既然要用 Proxy
實現,那麼確定得生成一個 Proxy
對象,所以咱們首先來實現一個生成 Proxy
對象的函數。code
// 用於判斷是否爲 proxy 對象
const isProxy = value => !!value && !!value[MY_IMMER]
// 存放生成的 proxy 對象
const proxies = new Map()
const getProxy = data => {
if (isProxy(data)) {
return data
}
if (isPlainObject(data) || Array.isArray(data)) {
if (proxies.has(data)) {
return proxies.get(data)
}
const proxy = new Proxy(data, objectTraps)
proxies.set(data, proxy)
return proxy
}
return data
}
複製代碼
value[MY_IMMER]
,由於只有當是 proxy 對象之後纔會觸發咱們自定義的攔截 get
函數,在攔截函數中判斷若是 key
是 MY_IMMER
的話就返回 target
Object
構造出來的對象或數組,isPlainObject
網上有不少實現,這裏就不貼代碼了,有興趣的能夠在文末閱讀源碼Map
中拿便可,不然就新建立一個。注意這裏用於存放 proxy 對象的容器是 Map
而不是一個普通對象,這是由於若是用普通對象存放的話,在取值的時候會出現爆棧,具體緣由你們能夠自行思考🤔接下來咱們須要來實現 proxy 的攔截函數,這裏有上文說過的兩個核心思路。
// 注意這裏仍是用到了 Map,原理和上文說的一致
const copies = new Map()
const objectTraps = {
get(target, key) {
if (key === MY_IMMER) return target
const data = copies.get(target) || target
return getProxy(data[key])
},
set(target, key, val) {
const copy = getCopy(target)
const newValue = getProxy(val)
// 這裏的判斷用於拿 proxy 的 target
// 不然直接 copy[key] = newValue 的話外部拿到的對象是個 proxy
copy[key] = isProxy(newValue) ? newValue[MY_IMMER] : newValue
return true
}
}
const getCopy = data => {
if (copies.has(data)) {
return copies.get(data)
}
const copy = Array.isArray(data) ? data.slice() : { ...data }
copies.set(data, copy)
return copy
}
複製代碼
get
的時候首先須要判斷 key
是否是 MY_IMMER
,是的話說明這時候被訪問的對象是個 proxy
,咱們須要把正確的 target
返回出去。而後就是正常返回值了,若是存在 copy 就返回 copy,不然返回原數據set
的時候第一步確定是生成一個 copy,由於賦值操做咱們都須要在 copy 上進行,不然會影響原數據。而後在 copy 中賦值時不能把 proxy 對象賦值進去,不然最後生成的不可變對象內部會內存 proxy 對象,因此這裏咱們須要判斷下是否爲 proxy 對象最後就是生成不可變對象的邏輯了
const isChange = data => {
if (proxies.has(data) || copies.has(data)) return true
}
const finalize = data => {
if (isPlainObject(data) || Array.isArray(data)) {
if (!isChange(data)) {
return data
}
const copy = getCopy(data)
Object.keys(copy).forEach(key => {
copy[key] = finalize(copy[key])
})
return copy
}
return data
}
複製代碼
這裏的邏輯上文其實已經說過了,就是判斷傳入的參數是否被修改過。沒有修改過的話就直接返回原數據而且中止這個分支的遍歷,若是修改過的話就從 copy 中取值,而後把整個 copy 中的屬性都執行一遍 finalize
函數。
最後一步就是把上文所說的函數所有整合在一塊兒
function produce(baseState, fn) {
// ...
const proxy = getProxy(baseState)
fn(proxy)
return finalize(baseState)
}
複製代碼
以上就是整個思路實現了,讓咱們來檢驗下是否能正常實現咱們想要的功能。
const state = {
info: {
name: 'yck',
career: {
first: {
name: '111'
}
}
},
data: [1]
}
const data = produce(state, draftState => {
draftState.info.age = 26
draftState.info.career.first.name = '222'
})
console.log(data, state)
console.log(data.data === state.data)
複製代碼
從上述代碼打印出的值咱們能夠看到 data
和 state
已經不是同一個引用,修改 data
不會引起原數據的變動,而且也實現了只淺拷貝修改過的屬性。對象中的 data
屬性由於沒有被修改過,全部兩個對象中的 data
仍是同一個引用,實現告終構共享。
擺上 源碼地址,其實 immer 內部遠不止這些實現代碼,其中會有更多的數據檢驗以及兼容性判斷,本文的代碼更多的是提供一種不同的深拷貝實現思路。
各位讀者有任何疑問或者其餘問題均可以在評論區中交流。