结构化克隆:浏览器的序列化机制
本文为 HTML 标准解读系列文章,其他文章详见这里。
以下的这几个 web API,虽然他们的功能各异,但是底层却有同样的机制,你知道什么吗?
history.pushState()
/history.replaceState()
:修改浏览器的历史堆栈。
window.postMessage()
:实现文档之间的跨域通信。Channel通信API:使用频道进行通信。
Channel广播API:同时给同一浏览器下多个同源的文档广播消息。
worker.postMessage()
:worker api,不同线程直接的交流,
没错,答案就是:结构化克隆(structured cloning)。
什么是结构化克隆?
一般来说,当一个页面unload
的时候,该页面内所有由 JS 创建的对象都会被垃圾回收。但有时候我们需要保留一些页面的状态,比如页面内动画的帧数,这样当用户返回的时候,动画能够接着上一次跳转出去时的地方开始播放,而不用从头开始。这也是history.replaceState()
的重要使用场景之一。
当你调用history.replaceState(state, null)
的时候,state
对象能够被浏览器缓存下来。待你之后通过历史导航按钮返回该页面时,你又能够在history.state
上重新拿到state
这个数据了。这是怎么做到的呢?
你的直觉可能已经告诉你答案了,就是使用序列化。如果你对「序列化」的概念不熟悉,其实 JavaScript 语言里就有一套JSON
的序列化机制。我们用JSON.stringify()
将 js 对象转化为便于存储、分发的 json 字符串,这个过程叫「序列化」;我们用JSON.parse()
将 json 字符串解析为 js 对象,这个过程叫「反序列化」。
而 HTML 也有一套自己序列化机制。与JSON.stringify()
不同的是,这套机制对于不同类型的 js 对象定义了不同「序列化」以及「反序列化」的算法/步骤,有的对象在序列化的过程只会保留一部分的属性,比如正则表达式lastIndex
属性的值会在序列化的过程中丢失;有的对象压根就不能被序列化,比如node节点
;而所有的这些算法加起来统称为结构化克隆,HTML标准的2.7小节对这套机制进行了详细的定义和阐述。
所以,简单的说,当调用history.replaceState(state, null)
的时候,浏览器会做以下的事情:
查看
state
的数据类型根据数据类型调用对应的序列化算法
而当你通过浏览器的历史导航返回该页面的时候,浏览器就会把state
反序列化的结果放在history.state
上,供你使用。
结构化克隆的限制
如果你想深入研究结构化克隆的算法步骤,即对于不同类型的对象会做什么样的操作,那你必须仔细阅读HTML标准2.7.3。不过,MDN 早就做了一个非常好的总结了:
结构化克隆所不能做到的:
我们开篇列举了一些 API,然后我们说这些 API 的底层都是使用结构化克隆来进行序列化的。换句话说,所有文章开头给出的 API,都受到上面这里列出的一模一样的限制。 比如,如果你分别执行以下不同的代码片段,他们会报一模一样的错误:HTMLDocument object could not be cloned.
:
既然结构化克隆的应用如此广泛,js 也给出一个相应的API:structureClone(value)
,调用这个方法,你就可以在你自己的程序中应用最原汁原味的结构化克隆的算法。
结构化克隆的另一面:转移对象
当我们仔细查看structureClone
api 的时候,我们发现它还接受第二个参数:
structuredClone(value, { transfer })
与此同时,其他前面提到的 web API,除了history
相关 API 以及Channel通信API,其他 API 也有这个transfer
参数:
window.postMessage(message, targetOrigin, transfer)
postMessage(message, transfer)
这里的 transfer,都是指 Transferable objects,可转移对象。
前面我们讲到的序列化,都是在克隆出对象的复制品。这在像Channel通信API这种需要给多个不同文档传输同一数据副本的场景下非常有用。但是,这有时候又会造成浪费,比如,如果你传输的对象是一个很大的二进制数据,更好的方法可能是「转移」这个对象,而不是花很大的力气去复制这个对象,这就是 Tranferable Objects 的使用场景。
Tranferable Objects 有以下特点:
转移的是对象在内存中的索引而不是复制对象;
原有的对象将会丢失在内存中的索引,不能再被使用,从而避免在不同环境下操作同一个对象的情况;
不是所有的对象都是 transferable object,MDN 列出了所有支持的对象 。
MDN 对此举了一个例子:
总结与延伸
在 IDL 片段中,所有支持序列化的接口会使用[Serializable]
这个扩展属性进行标记。而所有可转移对象的接口都会使用[Transferable]
来进行标记。(如果你不了解 web IDL 以及扩展属性,可以阅读我的另一篇文章一文读懂web标准的基石:web IDL)
举个例子,ImageBitmap接口就同时有这两个扩展属性,意味着它的实例即可以被序列化又可以被转移。
版权声明: 本文为 InfoQ 作者【水鱼兄】的原创文章。
原文链接:【http://xie.infoq.cn/article/055ee54fc4a83d3332180ef9c】。文章转载请联系作者。
评论