写点什么

结构化克隆:浏览器的序列化机制

作者:水鱼兄
  • 2022 年 10 月 10 日
    日本
  • 本文字数:2514 字

    阅读完需:约 8 分钟

本文为 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)的时候,浏览器会做以下的事情:


  1. 查看state的数据类型

  2. 根据数据类型调用对应的序列化算法


而当你通过浏览器的历史导航返回该页面的时候,浏览器就会把state反序列化的结果放在history.state上,供你使用。

结构化克隆的限制

如果你想深入研究结构化克隆的算法步骤,即对于不同类型的对象会做什么样的操作,那你必须仔细阅读HTML标准2.7.3。不过,MDN 早就做了一个非常好的总结了:


结构化克隆所不能做到的:

  • Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。

  • 对象的某些特定参数也不会被保留

  • RegExp 对象的 lastIndex 字段不会被保留

  • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。

  • 原形链上的属性也不会被追踪以及复制。


我们开篇列举了一些 API,然后我们说这些 API 的底层都是使用结构化克隆来进行序列化的。换句话说,所有文章开头给出的 API,都受到上面这里列出的一模一样的限制。 比如,如果你分别执行以下不同的代码片段,他们会报一模一样的错误:HTMLDocument object could not be cloned.


// 浏览器报同样的错: HTMLDocument object could not be cloned.// 代码片段1: 尝试pushState document对象history.pushState(document, null)// 代码片段2: 尝试给iframe传输document对象const o = document.getElementsByTagName('iframe')[0];o.contentWindow.postMessage(document, '*'); // 代码片段3: 尝试给worker线程传输document对象const myWorker = new Worker('worker.js');myWorker.postMessage(document)
复制代码


既然结构化克隆的应用如此广泛,js 也给出一个相应的APIstructureClone(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 有以下特点:


  1. 转移的是对象在内存中的索引而不是复制对象;

  2. 原有的对象将会丢失在内存中的索引,不能再被使用,从而避免在不同环境下操作同一个对象的情况;

  3. 不是所有的对象都是 transferable object,MDN 列出了所有支持的对象


MDN 对此举了一个例子


// 复制的过程const original = new Uint8Array(1024);const clone = structuredClone(original);console.log(original.byteLength); // 1024console.log(clone.byteLength); // 1024
original[0] = 1;console.log(clone[0]); // 0
// 转移的过程// 如果转移的对象不是一个transferable object会报错// 我们可以转移 Uint8Array.buffer.const transferred = structuredClone(original, { transfer: [original.buffer] });console.log(transferred.byteLength); // 1024console.log(transferred[0]); // 1
// 转移之后原来的 Uint8Array.buffer 就不能使用了console.log(original.byteLength); // 0
复制代码

总结与延伸

在 IDL 片段中,所有支持序列化的接口会使用[Serializable]这个扩展属性进行标记。而所有可转移对象的接口都会使用[Transferable]来进行标记。(如果你不了解 web IDL 以及扩展属性,可以阅读我的另一篇文章一文读懂web标准的基石:web IDL


举个例子,ImageBitmap接口就同时有这两个扩展属性,意味着它的实例即可以被序列化又可以被转移。

发布于: 刚刚阅读数: 5
用户头像

水鱼兄

关注

前端开发 2019.01.15 加入

还未添加个人简介

评论

发布
暂无评论
结构化克隆:浏览器的序列化机制_水鱼兄_InfoQ写作社区