探索JS原型链规律性可视化

全篇共 4891 字。按500字/分钟阅读,预计用时 9.8 分钟。此篇想法总访问 50 次,今日访问 1 次。

我曾经用Illustrator绘制过JavaScript原型链可视化信息图,也曾在最近用iPad手绘过一副JavaScript原型链的涂鸦。如今我借助Airglass.js开发的NodeLink可视化组件动态演示JS核心参考中各值之间的信息传递与关联关系。

传统手段

2016年10月6日,我使用Adobe Illustrator绘制的信息可视化图。当时我还在用shuaihua.cc这个域名,所以信息可视化图还保留这水印。2019年7月11日,我使用iPad绘制了一幅关于JavaScript原型继承知识的涂鸦。我很享受随意的涂鸦时创意灵感一点一点清晰的感受。

可视化节点引用关系

在线体验地址

JavaScript可信参考信息可视化完整图

同样的10月6日,我用Airglass.js库开发的NodeLink组件,继续讲述JavaScript中关于原型链的故事,而这次更像是我专门为JavaScript语言的整个核心知识体系所做的。这次我将《JavaScript权威指南》第三部分JavaScript核心参考的内容骨架可视化了出来。由于我还在继续往这幅可视化作品中增加内容,所以呈现的只是一个大的骨架,这并不是全部内容。

JS核心的梳理规则

《JavaScript权威指南》第三部分是参考文档,包括JavaScript语言核心定义的类、方法和属性。该文档根据类或者对象的名字,按照字母来排序。我在制作这幅可视化作品时,主要参考了第三部分的参考文档。我将这些类、方法和属性通过可视化的手段——节点、端口以及节点连线等视觉元素来描述。

书中这样描述《JavaScript权威指南》第三部分内容的梳理规则的:“JavaScript核心定义了一些全局函数和属性,比如eval()和NaN。从技术上讲,这些是全局对象的属性。但由于全局对象没有名字,因此直接将这些属性一起非限定名列举在参考文档中。为了方便查阅,在书中以“Global”作为存放全局属性的对象,这这幅可视化图中我也将“Global”作为一个单独的节点对待。并且你会发现“Global”是所有类、方法、属性引用的顶端,null是一切的开始。”

我基本上完全按照书中的梳理规则可视化出较为完整的JavaScript语言核心内容。如果你想让我从JavaScript的源头说起,那么这幅可视化作品正符合你的心意。但对于初学者,从起源开始讲起反而会让他们一开始就摸不着头脑。所以对于初学者,我常常觉得应该从最直观、最实用和离他们最近的地方开始讲起,再逐步深入,也渐行渐远。但对于中高级的JavaScript开发者来说,他们更喜欢从最深奥和抽象的地方一点点地梳理到距离自己最近也最直观的地方。

连我都觉得庆幸的是,这幅未完成的可视化作品,可以同时既能满足初学者的需要,也能满足中高级开发者的需要。怎么做呢?当你从左向右欣赏时,便是站在中高级开发者的角度梳理JavaScript语言核心知识。当你从右向左欣赏时,就是站在初学者的角度梳理了。null可以说是JavaScript中最深奥和抽象的存在,有经验的开发者很享受它的虚无犹如宇宙一样虚空却能创生万物。JavaScript语言将浏览器作为语言的运行环境时,Global化身为window全局变量成为初学者最熟悉最常用到的变量。

我先站在初学者的角度,即从右向左来介绍这幅可视化作品。有经验的开发者会自行从左向右解读。JavaScript语言核心没有规定全局变量应该关联给哪一个“名”,不过“名可名非常名”,JavaScript语言在不同的运行环境中会有各种各样的描述全局变量的“名”,不过它们总归都是同一个意思。在浏览器中,我们暂且命名它为window。

window牵引着全部的全局变量,这些JavaScript语言核心的全局变量可以按任何你觉得合理的规则分类。我把他们分为四类:

  • 全局类,如 Function、Object、Array。
  • 全局对象,如 Math、JSON。
  • 全局函数,如 eval()、isNaN()、parseInt()。
  • 全局值,如 Infinity、NaN、undefined、null。

如上面可视化作品所呈现的那样,以上四类全局变量被window牵引着。如果继续追溯这四类的源头,只需沿着一条从window的输入端口伸出的节点连接线向左探索就能发现真正被window引用的节点。从这幅我还未完成的可视化作品上可以看到,window引用的四类全局变量中,全局类(如Function和Object)和全局对象(如Math和JSON)通过节点连接线可视化出了引用关系。我们继续从类出发,探索JavaScript语言核心的源头。

全局类其实可以归到全局函数的集合中,因为JavaScript中的类其实是特殊的被装饰过的函数,他们和普通函数的不同在于类的prototype属性对象比普通函数的要丰富,不论是原生内置类(如Array或Object)或开发者定义的类(如Apple或Animal )。所以我把它单独分为一类以和普通函数区分开。但还是那句话,至于你想怎么分类都可以,只要能自圆其说即可,哪怕你说所有的全局变量和非全局变量都是归类为对象或更玄妙的说法:空也是可以说得通的。

使用new关键字调用函数便能生成这个函数的一个实例对象。这个实例对象的原型就是构造它的那个类的prototype,并且类的prototype属性还存在指向类自己的constructor属性。就拿简单的Object构造的普通对象实例来说,满足:

var a = {};
a.__proto__ === Object.prototype // true
a.__proto__.constructor === Object // true

实例化后的对象包含一个__proto__属性,他指向构造函数的prototype属性。当我们谈到对象的原型时,指的是构造出对象的类的prototype属性或者干脆就是指__proto__所引用的对象。但当我们谈到类的原型时,就要格外小心混淆概念。因为类其实是修饰过的普通函数,函数也是被类构造出来的,所以函数也有他自己的原型,并且函数的原型也是函数满足:

eval.__proto__ === Function.prototype // true
Object.prototype.toString.call(eval.__proto__) // true

容易混淆概念的点在于,当我们谈论类的原型时,谈论的到底是指类作为普通函数来讲的__proto__属性还是指类的prototype属性。当两个半斤八两的程序员在谈论这件事时,出现了理解上的分歧,嗡嗡半天,权利最终取得胜利。语言是最低档的沟通方式,你们不妨放下彼此的身段,真诚的拆掉心防,通过各种比语言更具说服力的可视化手段(如涂鸦)直观的沟通这件小事,而不是非要争个你死我活。讲真的,某些程序员业务能力差,但嘴皮子功夫好,但他们却热衷于“理论研究”,并总想制造出对一个事物理解上的鸿沟裂缝,吹毛求疵,然后想象着自己以一种颇具权威的身份登台亮相。

无论何种情景下,如果你发现有人只跟你屁话连篇,而不肯表示出真诚想要解决问题的态度和拿出任何实际的东西出来,你可以断定他就是我在上一段落描述的那种人无疑了。

到这里,我其实已经将上面可视化作品的中间部分全部介绍完毕。最后还剩一个null就可以连通整幅可视化作品。其实你应该能发现,从window开始(因为关系不大所以我没有画出window对象的原型,不过它也有的),每一条分支的探索都是在顺着__proto__向前进。现在我们来到了Object.prototype节点,继续前进便是寻找Object.prototype.__proto__的指向,结果是:

Object.prototype.__proto__ // null
Object.prototype.something // undefined

通常来说说,如果我们查询对象中不存在的属性引用,会返回undefined。而查询Object.prototype对象的__proto__属性却返回了null,说明__proto__存在于Object.prototype且指向null,即空对象。这里是原型链的最深处,是一切开始的地方。现在你可以从空对象这里出发向着window前进了。

关键帧提升动效性能

我在10月5日初步完成了这幅可视化作品后,10月6日开始写这篇文章。一直到10月15日,我都在研究如何提升动效性能这件事。现在是10月17日,我已经将Airglass.js优化到支持关键帧动画的程度了。就拿节点连接线的电流动效来举例。有一层canvas专用用来不断渲染更新后的电流流动状态以形成动态效果,而在之前的Airglass版本中,所有组件每一次在画布上渲染都会重新绘制一遍电流当前流经的位置。问题不在于重新在画布上渲染,而在于每渲染一帧画面都要以消耗性能为代价完成重复的计算。Airglass中关键帧依赖canvas离屏绘制技术,将动画状态提前一次性绘制在离屏canvas上,减少了重复计算当前动画状态的次数。

Airglass支持关键帧动画

现在Airglass默认支持关键帧动画。相同的一帧会被重复利用,极大的减少了相同状态帧画面的渲染,节省大量计算成本。任何事情走向极致都会适得其反。就像多喝水固然好,但水喝多了也会中毒。频繁重新计算和绘制动效在某些情况下固然方便,但每一帧的计算量都很大也势必影响渲染效果。缓存提前渲染好的关键帧减少重复计算固然好,但大量的关键帧也会吃垮内存。所以任何解决方案脱离了实际情况不所谓好与不好之说,在不同的场景选择最适合该场景的解决方案才是解决问题的不二法门。

拿节点连接线的电流动效举例。从起始点到终止点的所有电流流动状态作为关键帧一次性计算出来,就能大大节省在渲染电流效果时重复计算电流当前状态的性能开销。至于帧数,开发者可以任意调控,但仍不要忘了权衡解决方案与应用场景的匹配度是否最佳。

Airglass生成关键帧Sprite图

从动画理论上来讲,帧数越高意味着动画越流畅,相应地用户体验就越好。这里我并不是给所有节点连线动效设置相同的关键帧数,而是根据节点连线的长度计算出一个帧数相对合理关键帧集合。这里我遇到了一个有待我继续研究并解决的问题,随着帧数的升高,到一个临界值时,所有的关键帧都不在渲染了。我查看了任务管理器,CPU使用率很低,但内存占用量很大。我想这就是算法上讲的时间复杂度与空间复杂度的不同表现。

随着关键帧数的增加,运行当前页面所需要的内存也越来越多。我猜测是浏览器阻止了过高的内存请求,所以最终导致没有渲染任何关键帧画面。我还需要继续改进Airglass中关键帧的机制。初步想到的最佳实践方案是,在使用关键帧动画的前提下,尽可能多合并相关组件的关键帧,对于不需要支持动画的组件不许使用关键帧渲染,而采用需要更新组件状态时重新计算状态并渲染的方式。

经过我的研究发现,原先我按照window的devicePixelRatio属性值的比例将画布缩放,保证在视网膜屏下画质依旧清晰。于是我再次更新Airglass的一些组件,使得缩放比例可调控,开发者可以在画质与性能之间作出权衡。对于在超大屏幕下观看的用户来说,降低设备像素比对最终视觉上的呈现效果来说并不会影响多少。所以这个问题的最优解决方案还是在节省性能的前提下增加系统的内存大小,这是在对硬件配置提出较高的要求。

发布日期 » 2019年10月6日 周日
原创声明 » 请勿复制转载,谢谢配合。
Airglass.js核心库
JavaScript核心概念
硬件编程、Arduino
文档翻译计划
微信开发
前端脚手架
运维
可视化
生活自有“道”理
视觉设计、用户体验
陈帅华的微信二维码