Skip to content

Power-kxLee/proxyCopyCode

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

感受Proxy的魅力-深拷贝(小白看得懂系列)

背景

最近找下深拷贝的资料,阅读了一些资料,看得不太清楚,大部分人云亦云,学习最主要抱着学精的态度,不怕慢 最主要能学进大脑 故整理了一些心得经验,就有此文,选择了个人认为最好的一种深拷贝方式proxy(如果你有更好的办法 欢迎不吝指教)

本文描述以最基础开始,尽量直白简单化,带领你走入Proxy的第一步,从而也能够认识Proxy的魅力

开始

什么时候用到深拷贝?你也许会遇到过这种 后台返回了一堆Object,你需要按照设定好的算法 去返回某部分的对象,但是又不想破坏掉原对象 所以你可能会通过

  • JSON.stringify(Object)把原对象转成字符串,但是会有很多局限性
  • 另外一个就比较复杂, 可以查看Lodash的实现方式

先来看看 我们需要浅拷贝怎么样做? 看一段例子 对象是引用类型,顾名思义,也就是互相引用,所以我在赋值进行修改,会影响原来的对象

//对象 
let o = {a:1}
let _copyO = o
_copyO.a = 2
o.a // 2

//数组
let arr = ['a'] 
let _copyArr = a 
_copyArr = ['b']
arr // ['b']

所以这里可以用浅拷贝的方式,能很简单的达到我们的目的

//对象 
let o = {a:1}
let _copyO = {...o}
_copyO.a = 2
o.a // 1

//数组
let arr = ['a'] 
let _copyArr = arr.slice()
_copyArr = ['b']

arr  // ['a']

这里核心的拷贝原理还是基于最简单粗暴的:

slice() 方法可从已有的数组中返回选定的元素。不会改变原始数组

浅拷贝

我们知道了原理之后, 就可以通过Proxy来实现浅拷贝

如果你不太了解Proxy的用法,建议先去MDN刷一遍

思考: 为什么要用Proxy去实现拷贝? 我明明只修改了这个属性,为什么不能够只对这部分的属性做拷贝呢? 这里就是Proxy代理的魅力了。

当然你也可以用Object.defineProperty,但是这只能对某个属性改变了而作出响应,而Proxy是代理了当前的对象

先走一个浅拷贝

const people = {
  man: {
    name: 'lee',
    body: {
      foot: {
        state: 'good'
      },
      hand: {
        state: 'good'
      }
    }
  },
  friend: ['jw', 'gg', 'gw']
}
  
const copyMap = new Map() 
const _handle = {
  get(target, prop) {},
  set(target, prop, value) {
    const _cope = Array.isArray(target) ? target.slice() : {...target} // 复制数据
    _cope[prop] = value
      // 这里很重要 保存 复制后的数据 方便调取
    copyMap.set(target, _cope)
    
    return true
  }
}
let _py = new Proxy(people, _handle)
_py.friend = ['jw']

let p = copyMap.get(people)
console.log(p)
console.log(people)

运行上面代码, 可以看到已经被拷贝,不会影响了原对象 运行结果

p => {man: {…}, friend: ["jw"]}
people => {man: {…}, friend: ["jw", "gg", "gw"]}

在这里我想修改man这个属性

_py.friend = ['jw']
_py.man = {}

let p = copyMap.get(people)
console.log(p)
console.log(people)

然后在看看输出

{
friend:["jw", "gg", "gw"]
man: {}
}

man这个属性被修改了,而friend这个属性没有变化 原因是因为 copyMap.set(target, _cope) 永远就更新了最后一个, 所以通过copyMap根据原对象取值的时候, 就是最后一个的变化 只需要进行一些改动

const copyMap = new Map()
const _handle = {
  get(target, prop) {},
  set(target, prop, value) {
    let _copy
    // 这里如果被保存过在copyMap里面 就不要在另外复制一个 而是用当前这个进行再次修改
    if (copyMap.has(target)) {
      _copy = copyMap.get(target)
    } else {
      _copy = Array.isArray(target) ? target.slice() : { ...target } // 复制数据
    }
    _copy[prop] = value
    // 保存每一段修改后的数据
    copyMap.set(target, _copy)
    return true
  }
}

let _py = new Proxy(people, _handle)
_py.man = {}
_py.friend = ['jw']
let p = copyMap.get(people)
console.log(p)
console.log(people)

原理就是,把拷贝的数据存到new Map(),更新的属性反响到拷贝的数据。最后就取拷贝好的对象就可以。 这就是用proxy来处理浅拷贝的原理,只对更新的属性这部分做出相应,性能达到最大化

深拷贝

了解了浅拷贝之后,就可以看看重点深拷贝拉

还是沿用上面的例子,我们来添加多一级的属性修改_py.man.name = 'xiaoMing' 会发现有报错

Uncaught TypeError: Cannot set property 'name' of undefined

因为根本没有进入到set()这个捕捉器,我理解是,这条链子太长了,手够不着,只能获取到最后一个属性更新的value 但是get()这个捕捉器刚好相反,你够不着,我都能够摸得到。并能获得每一次属性的值 上个例子

const copyMap = new Map()
const proxyMap = new Map()

const _handle = {
  get(target, prop) {
    console.log('get捕捉器', target, prop)
    const _data = copyMap.get(target) || target
    const _proxy = setProxy(_data[prop])
    return _proxy
  },
  set(target, prop, value) {
    console.log('set捕捉器', target, prop, value)
    let _copy  
    if (copyMap.has(target)) {
      _copy = copyMap.get(target)
    } else {
      _copy = Array.isArray(target) ? target.slice() : { ...target } // 复制数据
    }
    _copy[prop] = value
    copyMap.set(target, _copy)
    return true
  }
}

// 把转换成proxy的复用 写成一个函数去处理
const setProxy = (target) => {
  // 检测是不是普通对象 或者 是数组
  if (Object.prototype.toString.call(target) === '[object Object]' || Array.isArray(target)) {
    if (proxyMap.has(target)) {
      return proxyMap.get(target)
    }
    const _proxy = new Proxy(target, _handle)
    proxyMap.set(target, _proxy)
    return _proxy
  }
  return target
}

let _py = setProxy(people)
_py.man.name = 'xiaoMing'
let p = copyMap.get(people)
console.log(p) // undefined 

需要在get捕获器去处理,每次进来的属性值 都转成对应的proxy对象 这里新增一个setProxy()方法,需要复用相同的转换proxy的方法

  • 定义了一个proxyMap的变量,是Map类型
  • 每次新增都保存到proxyMap的栈中
  • 如果已在proxyMap的栈中匹配到相同的,直接取出来使用

看看上面的结果, 输出的是undefined,因为copyMap保存的是man这个拷贝对象,并是已经做了拷贝赋值的对象man.name = 'xiaoMing'

在回头看看Proxy的核心,我只对操作的这部分属性做出相应,所以就只保存这部分做出相应内容

来个看形容点情景

你做出了一盘好看的水果盘,里面都是你喜欢吃的水果,其他商家抄袭了你的水果盘

那么你就继续创新去变化更好看的水果盘,可恶的商家也跟着变

有办法了! 我把需要变化的部分单独保存,最后在拼接成另外一个水果盘

我只需要关系我需要变动的这部分,也减少了我的工作量

大概是这个意思,绞尽脑汁才想出来的

回到例子,所以这里的而people对象没有被保存,只保存了需要变化的部分

那么就要把people这个原始对象丢进去处理,拷贝一份出来 这里写多一个函数处理Map

const _handleMap = (target) => {
  // 只处理普通对象 和 数组  因为是递归循环 最后肯定会获得单个数值的 所以就直接只处理对象或者数组就可以了
  if (Object.prototype.toString.call(target) === '[object Object]' || Array.isArray(target)) {
    let _copy
    const copy = Array.isArray(target) ? target.slice() : { ...target } // 复制数据
    // 这里处理copy的缓存
    if (copyMap.has(target)) {
      _copy = copyMap.get(target)
    } else {
      copyMap.set(target, copy)
    }

    const _co = copyMap.get(target)
    // **这里是重点
    // 递归循环对象的每一个键值 然后在copyMap里取出来 赋值回去
    // 这里就可以做到深拷贝拉
    Object.keys(_co).forEach((item) => {
      _co[item] = _handleMap(_co[item])
    })
    return _co
  }
  // 其他类型就直接返回 比如字符串 函数 数值什么的
  return target
}

let _py = setProxy(people)
_py.man.name = 'xiaoMing'
_handleMap(target)

let p = copyMap.get(people)

也许你看到这里有点懵,但是这是整合深拷贝的最后一个核心方法,下面再也没其他操作拉 先来解析看看

  • people丢到函数进行加工处理,得到一个拷贝的对象,并存到copyMap里面
  • 开始循环people对象的key值,然后根据key匹配到对应的变化的部分在添加回去
  • 最终就构建了一个完整 拷贝对象,根据原对象去获取就可以

整个深拷贝到这里结束

优化

你会发现, copyMap为什么连body下的属性也添加了进去,不是说了 只操作变化的部分吗? 那这没区别啊?还是要循环整个对象

所以这里得加多一行代码 去进行处理

const _handleMap = (target) => {
if (Object.prototype.toString.call(target) === '[object Object]' || Array.isArray(target)) {
  // 没有被map保存过的,也就是肯定没有被操作过的
  if (!(proxyMap.has(target) || copyMap.has(target))) {
    return target
  }
  let _copy
  const copy = Array.isArray(target) ? target.slice() : { ...target } // 
  if (copyMap.has(target)) {
    _copy = copyMap.get(target)
  } else {
    copyMap.set(target, copy)
  }

  const _co = copyMap.get(target)

  Object.keys(_co).forEach((item) => {
    _co[item] = _handleMap(_co[item])
  })
  return _co
}
// 其他类型就直接返回 比如字符串 函数 数值什么的
return target
}

这样就可以提前拦截,没有被操作的属性直接忽略,不去执行递归,从而达到了性能最大化

最后还可以优化一下代码,把复用的代码抽出来,比如const copy = Array.isArray(target) ? target.slice() : { ...target }这一段的代码逻辑 最终还需要封装成函数对外调用

源码地址:(配合源码食用更合适) https://github.com/Power-kxLee/proxyCopyCode

总结

  • 这是我见过最高性能的深拷贝, 只对变化的这一部分进行操作
  • 所以需要知道操作得是那部分,Prxoy在这里完全是C位
  • 最终整合变化的部分,拷贝一个新的变化对象

参阅:

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages