循环引用的对象不能被序列化以及其拷贝方式

289 阅读3分钟

一、前言

element-plus el-tree

1.1 循环引用

有两个对象:objA 和 objB,其中 objA 的一个属性指向了对象 objB,而对象 objB 也有一个属性指向了对象 objA,这就造成了循环引用。示例:

const objA = {}
const objB = {}

objA.root = objB
objB.store = objA

console.log('对象间循环引用-objA:', objA)
console.log('对象间循环引用-objB:', objB)

ES6 引入了 Class 概念,类的本质是函数(function),类的实例是对象(object)

cosnt store = new TreeStore()
console.log(typeof TreeStore, typeof store) // funciton object

JSON.stringify() 无法将一个造成循环引用的对象序列化,这种循环引用:无穷无尽,没有尽头

console.log(JSON.stringify(objA))

报错如下:

image.png

二、el-tree

2.1 问题描述

el-tree 中有两个类:TreeStore 和 Node,TreeStore 类存储了树的数据,Node 类生成树节点结构。但是 TreeStore 有一个属性 root,是树的根节点,指向 Node;而树节点 Node 里也有一个属性 store 指向了 TreeStore。 在看这块代码过程中,遇到一个问题:

image.png

代码如下:

<template>
  <div>
    {{root}}
  </div>
</template>

<script>
import { defineComponent, ref } from 'vue'
import TreeStore from './model/tree-store'

export default defineComponent({
  name: 'ElTreeInit',
  props: {
    data: Array,
    nodeKey: String,
    props: {
      type: Object,
      default: () => ({
        children: 'children',
        label: 'label',
        disabled: 'disabled',
      }),
    },
  },
  setup(props) {
    const store = ref(
      new TreeStore({
        data: props.data,
        key: props.nodeKey,
        props: props.props,
      })
    )
    store.value.initialize()
    
    const root = ref(store.value.root)
    console.log('props', store.value, root)
    
    return {
      root,
    }
  },
})
</script>

2.2 问题处理

真的,这个问题我找个好久

(1)起初,将 template 里引用 root 的这行代码删掉,报错便没有了,控制台 console.log 的 store 也是正常的,从树根到整棵树的结构都已呈现出来。但是加上这行代码,便产生报错。又仔细看了源码,很困惑。

(2)后来,根据报错的信息去找问题,发现了循环引用

(3)最后,顺着循环引用,发现循环引用的对象不能通过 JSON.stringfy() 序列化,而 template 里的对象会被执行 JSON.stringfy(),因为无穷无尽没有尽头,所以报错。若是访问非循环引用对象,可避免此报错。

el-tree 源码里并没有在 template 里访问循环引用对象(store.root 和 node.store),在 tree.vue 遍历 root.childNodes 循环调用 tree-node.vue,而 tree-node.vue 循环调用传入的节点 node.childNodes 构造子树。

// tree.vue
<template>
  <div>
    <!-- {{ root }} -->
    <tree-node
      v-for="child in root.childNodes"
      :key="getNodeKey(child)"
      :node="child"
    />
  </div>
</template>

// tree-node.vue
<template>
  <div>
    <div>{{ node.label }}</div>
    <div>
      <tree-node
        v-for="child in node.childNodes"
        :key="getNodeKey(child)"
        :node="child"
      />
    </div>
  </div>
</template>

三、实现对循环引用对象的拷贝

额外开辟一块存储空间weakMap(为什么使用WeakMap?因为它相对Map是弱引用),来存储当前对象target和拷贝对象cloneTarget之间的对应关系。当需要拷贝当前对象target时,先去weakMap中找,若找到则直接返回,若没有找到则继续拷贝并且把拷贝结果存储weakMap

/**
 * 判断是否是对象且排除null
 * @param {*} target 
 * @returns 
 */
function isObject(target) {
    return typeof target === "object" && target !== null
}
/**
 * 深拷贝对象
 * @param {*} target 
 * @param {*} weakMap 
 * @returns 
 */
function deepClone(target, weakMap = new WeakMap()) {
    if (isObject(target)) {
        // ==对象==
        let cloneTagret = Array.isArray(target) ? [] : {}
        const cache = weakMap.get(target)
        if (cache) {
            return cache
        }
        weakMap.set(target, cloneTagret)
        for (let curKey in target) {
            cloneTagret[curKey] = deepClone(target[curKey], weakMap)
        }
        return cloneTagret
    } else {
        // ==基本数据类型==
        return target
    }
}
let target = {
    idCard: 1,
    name: "露水晰123",
    address: "中国",
}
// ==设置循环引用==
target.parent = target
console.log("深拷贝循环引用对象", deepClone(target))
console.log("深拷贝循环引用对象", deepClone(target) === target) // false

控制台打印结果:

1732160811077.png

四、总结

  • 对象间的循环引用:对象A里的一个属性指向了对象B,而对象B也有一个属性指向对象A,从而造成循环引用,这两个对象也是循环引用对象;
  • 类的本质是 funciton,类的实例是 object;
  • JSON.stringfy 无法将一个循环引用对象序列化(因为会无穷无尽,没有尽头)。
  • WeakMap相对Map式弱引用,弱引用指key若被垃圾机制回收则对应的value也不存在。

五、参考