轻量高效的拓扑图组件
zh

JS序列化 JSONSerializer

2021-03-08 2022-04-28 最后修改

js默认序列化存在的问题

js默认序列化使用JSON.stringify / parse(),存在几个问题

  • 1、只支持基本类型,不支持function,类等
  • 2,不支持引用,比如value = [1,2,3], 序列化:{a: value, b: value},则value会重复输出
  • 3,不支持嵌套引用,如果对象自己引用自己,会出错

另外可以通过重写#toJSON(propertyName?),自定义序列化输出

解决方案

无法直接解决以上问题,提供其他实现方式

全局设置#setJSName(name, objectOrClass)

增加全局函数setJSName(name, objectOrClass),给类或者对象一个唯一名称或者id,这样可以根据名称找到这个类或者对象,设置的这些对象存放在全局JS_Map中 比如:

setJSName(“Q.LabelUI”, LabelUI);
setJSName(“Q.Node”, Node);
setJSName(“Q.Position.LEFT_TOP”, Position.LEFT_TOP);

关键词:_path, _class

使用关键词:_path, _class,输出类或者函数,在json输出时,上面的名称可用在两个地方

  1. _path, 直接输出 比如
{position: Position.LEFT_TOP}
toJSON
{position: {_path: “Q.Position.LEFT_TOP”}}
  1. _class输出 比如:
let node = new Q.Node();
toJSON
{
_class: “Q.Node”,
…//其他属性
}

_path属性表示直接从全局JS_Map获取到此对象,{_path: “Q.Position.LEFT_TOP”} 就表示Q.Position这个对象 类似的也可以输出函数

function fn1(){}

setJSName(‘fn1’, fn1)

输出就是:{_path: “fn1”}

_class表示类型,在解析时,可根据_class信息创建相应类型的对象,比如:{_class: ‘Q.Node”} 会创建 let node = new Q.Node(),构造完成后再解析其他的属性

使用关键词_refId, _ref,实现引用的问题

通过_refId设置对象的id 比如:{_class: ‘Q.Node”, _refId: 1},表示一个节点对象,引用id为1,如果后面用到这个对象,则可以直接用_ref: 1表示此节点 比如:{_class:’Q.Group’, children: [{_ref: 1}],表示一个分组对象,前面的节点是他的孩子

#toJSON()自定义输出

通过前面的几种方式,已经可以将一个特定类对象或者函数进行输出,比如类对象,默认会输出_class类型以及for in循环的属性,类似下面的样子

{
_class: ‘Q.Node’,
name: ‘Qunee’,
x: 100,
y: 100,
styles: {
‘lable.color’: ‘#f00’
}
}

_class以外的属性都通过for in循环获取,也可以重写#toJSON函数,自己控制哪些属性输出,比如只输出styles信息

Q.Node#toJSON(){
return {
styles: this.styles
}
}

初期考虑过在toJSON(serializer)通过参数传入序列化生成器,这样可以方便对属性对象序列化,比如:

Q.Node#toJSON(serializer){
let json = {};
for(let name in this.styles){
json[name] = serializer.toJSON(this.styles[name]);
}
return json;
}

这样的写法存在两个问题:

  1. 不兼容默认的js序列化函数(默认并没有serializer对象);
  2. 写起来麻烦,需要自己保证每个属性的序列化结果

所以最后没有传入任何参数,而是将结果交给serializer再处理一次,性能上会有所影响,但是用起来更方便,也保持了兼容

#parseJSON(json) 自定义解析

默认通过for in 遍历,将属性设置给新创建的对象,类似下面的代码

for(let name in json){
object[name] = parseJSON(json[name]);
}

如果属性不能直接设置,或者希望自定义解析,可以通过#parseJSON(json, parser)函数实现,与#toJSON()函数形成对应关系

_json关键词,Array数组对象的导入导出

如果数组不被其他对象引用,可以按默认的格式[a,b … ]直接输出

如果有引用,需要加入refId信息,[]语法无法设置属性,需要改成{}格式,类似{_class: ‘Array’, _refId: 1, _json: [a, b …]}的格式

对象开始导出时,并不知道自己是否会被别的对象引用,对于普通的Object可以在后期直接追加属性,但是对于数组[],无法直接增加属性,所以我们定义了RefArray类,用于处理数组的输出

这里引入了关键词“_json”,表示实际的json数据,这种方式在自定义#toJSON()输出时也可能用到,当toJSON()返回的不是Object对象({…})时,比如number, string, [],这时候可以将toJSON()的结果放在_json属性中,parseJSON时优先传入_json属性,也就是parseJSON(json._json || json)

额外细节

属性遍历 关于关键词,_class, _path, _refId, _ref, json都是””开头,一般下划线开头表示私有属性,不会输出到json,所以这里的关键词不会与正常的属性冲突 默认属性遍历也会排除_开头的,另外对于getter/ setter 方式定义的属性,for in不会遍历到,这时候需要重写toJSON实现自定义输出

关于避免嵌套引用的技术细节,关键技术点是:

1、将构造和属性分开 2、导出对象之前就设置refId,方便后面使用 3,导入时,先构造得到实例对象,后解析属性

比如自己引用自己的情况:let a = new Q.Node(), a.ref = a; 输出

_class: ‘Q.Node’,
_refId: 1,
ref: {_ref: 1}
}

老版本问题

旧版本中通过扩展类JSONSerializer实现图元的导入导出,输出格式与现在的方案类似,关键词存在下面的区别

_className 改为 _class _classPath 改为 _path json 改为 _json,老版本自定json全部放在json属性中,新的代码实现只有在json不是Object时才会放在_json属性中

$ref 改为 _ref 删掉了refs块儿

JSON序列化待改进

存在的问题

1,如何增加某些属性,getter / setter定义的属性无法被遍历,也就无法被导出

getter设置enumerable:true

2,如何排除某些属性

Next Prev