你不知道的高性能实现深拷贝的方式

文章首发自 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 内部远不止这些实现代码,其中会有更多的数据检验以及兼容性判断,本文的代码更多的是提供一种不同的深拷贝实现思路。

各位读者有任何疑问或者其余问题均可以在评论区中交流。

相关文章
相关标签/搜索