你不知道的高性能實現深拷貝的方式

文章首發自 Githubgit

傳統深拷貝的問題

JS 中有個重要的類型叫作引用類型。這種類型在使用的過程當中,由於傳遞的值是引用,因此很容易發生一些反作用,好比:github

let a = { age: 1 }
let b = a
b.age = 2
複製代碼

上述代碼的寫法會形成 ab 的屬性都被修改了。你們在平常開發中確定不想出現這種狀況,因此都會用上一些手段去斷開它們的引用鏈接。對於上述的數據結構來講,淺拷貝就能解決咱們的問題。數組

let b = { ...a }
b.age = 2
複製代碼

可是淺拷貝只能斷開一層的引用,若是數據結構是多層對象的話,淺拷貝就不能解決問題了,這時候咱們須要用到深拷貝。數據結構

深拷貝的作法通常分兩種:函數

  • JSON.parse(JSON.stringify(a))
  • 遞歸淺拷貝

第一種作法存在一些侷限,不少狀況下並不能使用,所以這裏就不提了;第二種作法通常是工具庫中的深拷貝函數實現方式,好比 loadash 中的 cloneDeep。雖然這種作法能解決第一種作法的侷限,可是對於龐大的數據來講性能並很差,由於須要把整個對象都遍歷一遍。工具

那麼是否能夠有一種實現的作法,只有當屬性修改之後纔對這部分數據作深拷貝,又能解決 JSON.parse(JSON.stringify(a)) 的侷限呢。這種作法固然是存在的,惟一的點是咱們如何知道用戶修改了什麼屬性?性能

答案是 Proxy,經過攔截 setget 就能達到咱們想要的,固然 Object.defineProperty() 也能夠。其實 Immer 這個庫就是用了這種作法來生成不可變對象的,接下來就讓咱們來試着經過 Proxy 來實現高性能版的深拷貝。ui

實現

先說下總體核心思路,其實就三點:spa

  • 攔截 set,全部賦值都在 copy (原數據淺拷貝的對象)中進行,這樣就不會影響到原對象
  • 攔截 get,經過屬性是否修改的邏輯分別從 copy 或者原數據中取值
  • 最後生成不可變對象的時候遍歷原對象,判斷屬性是否被修改過,也就是判斷是否存在 copy。若是沒有修改過的話,就返回原屬性,而且也再也不須要對子屬性對象遍歷,提升了性能。若是修改過的話,就須要把 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
}
複製代碼
  • 首先咱們須要判斷傳入的屬性是否是已經爲一個 proxy 對象,已是的話直接返回便可。這裏判斷的核心是經過 value[MY_IMMER],由於只有當是 proxy 對象之後纔會觸發咱們自定義的攔截 get 函數,在攔截函數中判斷若是 keyMY_IMMER 的話就返回 target
  • 接下來咱們須要判斷參數是不是一個正常 Object 構造出來的對象或數組,isPlainObject 網上有不少實現,這裏就不貼代碼了,有興趣的能夠在文末閱讀源碼
  • 最後咱們須要判斷相應的 proxy 是否已經建立過,建立過的話直接從 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 對象
  • 建立 copy 的邏輯很簡單,就是判斷數據的類型而後進行淺拷貝操做

最後就是生成不可變對象的邏輯了

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)
複製代碼

從上述代碼打印出的值咱們能夠看到 datastate 已經不是同一個引用,修改 data 不會引起原數據的變動,而且也實現了只淺拷貝修改過的屬性。對象中的 data 屬性由於沒有被修改過,全部兩個對象中的 data 仍是同一個引用,實現告終構共享。

最後

擺上 源碼地址,其實 immer 內部遠不止這些實現代碼,其中會有更多的數據檢驗以及兼容性判斷,本文的代碼更多的是提供一種不同的深拷貝實現思路。

各位讀者有任何疑問或者其餘問題均可以在評論區中交流。

相關文章
相關標籤/搜索