likes
comments
collection
share

PageSpy 揭秘:如何打印 window 对象

作者站长头像
站长
· 阅读数 23

你可以暂时用不上,但你一定要知道的 Web 远程调试工具!

开源仅仅两天 Star 数量就突破 100+ 的 PageSpy,你还在等什么?!

赶紧点击主页链接前去体验吧。

前言

PageSpy 自开源提供服务以来,从社区中收到大量用户反馈说想了解 PageSpy 的工作原理,这么真挚而又热烈的要求,我们第一时间安排上了。本章就为大家带来深入 PageSpy 实现原理系列之「洞悉 Console 调试面板」。

Console 面板近乎还原了 Chrome Devtools 里控制台同级别的交互体验,除了打印日志外,还支持调试远端 Web 运行现场的变量信息。

功能方面大家可以体验到,我将和大家一起进入主题:PageSpy 如何处理 window 对象的打印

设计目标

PageSpy Console 面板最起初的设计目标就是向浏览器控制台的 Console 面板看齐。但是用户如果打印复杂对象,单纯靠源数据会面临以下问题:

  • 不支持序列化的类型数据会丢失;
  • Getter 属性值动态计算的交互特性会丢失;
  • self-reference 的对象序列化时会报错;
  • 无法沿着原型链查看数据明细;

这里面每一项在各自的场景中都发挥着无比重要的作用。当前可以明确的是:

  1. 想要打印复杂对象,肯定是需要先转换再使用;
  2. 由于后续还有查看数据的需求,所以还要找个地方存起来。

在 PageSpy 中,我们把负责对复杂对象进行转换、存储、查询、交互的功能体封成 Atom 类,每个复杂对象都是一个 atom—node,其结构体如下所示:

// SpyAtom.Overview
interface Overview {
  id: string;
  type:
    | 'string'
    | 'number'
    | 'bigint'
    | 'boolean'
    | 'symbol'
    | 'undefined'
    | 'object'
    | 'function'
    | 'null'
    | 'error'
    | 'debug-origin'
    | 'atom';
  value: string | PropertyDescriptor;
  __atomId?: string;
  instanceId?: string;
}

第一眼看上去没什么感觉完全没关系,我也是在写下这段文字的时候才被惊到……因为我想告诉你正是这 20 行类型代码决定了本次主题中的具体实现:打印 window 对象就是依赖这套接口类型定义。

为什么要强调打印的是 window 对象?因为 window 对象具备上述所有的「问题」于一身:

  • 有不支持序列化的属性数据,比如全局方法;
  • 有很多 Getter 属性,如 outerWidth / innerWidth / location 等;
  • self-reference,如 window.self
  • 沿着原型链查看数据明细

如何实现

我们在上面提到了:「打印复杂对象时,单纯靠源数据会面临……」,那么打印不复杂的对象呢?还会有那么多问题吗?在回答问题之前,我们先给出对于数据「复杂不复杂」的定义:

  • 可以点击查看明细的数据都是复杂数据,如 Object / Array / Set / Map 等;
  • 一眼就看明白的都是不复杂数据,如 "Hello" / 12345 / true / Symbol(foo) / Error / undefined 等;

那打印不复杂数据还会有那么多问题吗?答案是不会,对于这些不复杂的数据我们只需要拿到内容,并获取到它的类型即可完成渲染。

明确了数据复不复杂之后,我们从以上类型定义中把复杂数据类型的定义单独拎出来:

// SpyAtom.Overview
interface Overview {
  id: string;
  type: 'atom';
  value: string | PropertyDescriptor;
  __atomId: string;
  instanceId: string;
}
  • id:作为数据的唯一标识。

  • __atomId:打印对象时,对象实体会被记录在 Atom 的 storeMap 中,__atomId 在 storeMap 中作为 key,对应一个对象源数据。

  • instanceId:名称代表功能定义,本质的值由 __atomId 衍生。默认和 __atomId 相等、指向当前打印出来的对象;点击对象展开,每个内层属性都有一个 instanceId 指向父级(上一层)的对象,如果内层某个属性的属性值是对象,那么该孙子对象的 instanceId 不会指向父级。

除了以上三个 id 之外,我们还用到了一个被称为 parentId 的数据:

  • parentId:名称代表功能定义,本质的值也是由 __atomId 衍生。用于获取 Getter 属性值,具体的获取行为原理是 Object.getOwnPropertyDescriptor(store[parentId], key).get.call(store[instanceId])

总结

通过以上的内容介绍,我们再来回顾下打印 window 对象会遇到的问题,现在都有了相对应的答案:

有不支持序列化的属性数据?

答:通过获取类型将数据以「复杂」维度区分,结合数据值来描述一个数据该如何显示;

Getter 属性动态计算属性值的交互特性会丢失?

答:Getter 属性值的获取本质上是调用函数,需要注意的是函数体会存在使用 this 指针的情况。那么基于以上复杂数据结构的定义,我们知道了实例对象、键名,组合调用一下就能完美解决;

self-reference 对象无法序列化,如 window.self

答:PageSpy 只会转化当前层级的数据,也就是说,如果你的自引用是个对象,它的获取过程是个惰性转换并求值的。

沿着原型链查看数据明细?

答:虽然 descriptors 描述符不会给我们原型对象的数据,但是只需要我们手动添加一个 [[Prototype]] 加上去就可以满足用户沿着原型链点击查看数据明细啦!

以上就是本次深入 PageSpy 实现原理系列文章的全部内容了,欢迎大家在工作中集成使用,遇到问题可以在 Github 反馈,我们将第一时间解答。

转载自:https://juejin.cn/post/7241461849354207292
评论
请登录