自从我 对C语言有过编译原理上的实践经验 && 对js和浏览器的表层(非chromium源码级我都统称表层) 有了更深的理解后,对于JS及其相关的很多事物愈发的产生了疑惑,比如"执行上下文"、"渲染流水线"、"浏览器多进程到底怎么玩的(ipc等)"等等,网上现有的理论(包括ES官方),已经不能解决我产生的各种疑惑了。我需要的是chromium源码级的解释!!
我想先解决"执行上下文"等疑惑,又奈何,截止2021.11.1(我得严谨一点嘛,世事难料,我们永远不知道下一刻会发生什么,更何况过去了一个月呢)我竟然没有搜到一篇关于"执行上下文"的源码级解释(我需要的不是八股文也不是ES官方规范呐)。准确说是关于Chromium源码解释的资源本身就太少太少太少了
为什么要读源码呢?大家要先明白,ES规范的官方标准,它只是一个理论标准。它也是在某个技术实现后,才出的标准。所以最终实现上是怎样的婀娜多姿,只有相应开发者知道。其他人只能通过阅读源码窥得一二。而且源码中充斥着大量的最佳实践,以及为了极限性能而各种"骚操作"(导致难以阅读),可以瞻仰瞻仰。
没辙了,由于自身强烈的强迫症,我只好硬着头皮自己上了(虽然只有大一的C基础,也没学过C ,并且我也只是个仍在实习的小FEer。但是!这并不能阻挡我探索chromium的决心,Fighting!Fighting!)。 经过一个多月(每天完成公司任务后,自己加的2~3hour)对chromium的折腾(虽然本地都准备好了,但未能在本地调试。于是我在 chromium 【需要科学上网】,干巴巴的读。文末对chromium的阅读经验中会解释为何未能调试)。
打个不恰当的比方:如果其他矿工无暇分享来自chromium这座矿山的"资源",那我愿意学着如何去当一名矿工。戴上小钢帽儿,背上行囊,提起小铁铲儿,把挖到的、探索到的"资源"分享给大家。
Hi~ 我是本文向导
(: 后来想了想,不太推荐纯小白观看噢,纯小白慎入。最好是熟悉js&懂一点编译和c 。编译和C 不了解也没关系,了解js就行,其他的我来讲
首先关于 chromium 和 chrome 的关系大家可自行搜索哈。
“无忌,我教你的还记得多少?”“回太师傅,我只记得一大半” “ 那,现在呢?”“已经剩下一小半了” “那,现在呢?”“我已经把所有的全忘记了!” “好,你可以上了…”
先忘掉你之前学过的js上下文和作用域等知识,包括ES官方说的,也忘掉!我只能说,官方描述的"样子"源码里有对应的体现,但是具体称呼、约定、实现,官方无法强约束。再次强调,是chrome(或各大浏览器厂商)先有的实现,然后ES组织后来才出的官方标准。所以希望你最好和我一起,我们重新探索,重新学习。
本篇文章主要是从chromium源码中的v8(JavaScript引擎,主要内容都是由C 实现,还涉及Chrome自研的Torque语言.tq),去梳理js上下文&作用域、对象及数组等内容。还会涉及一些编译原理的知识。为了尽量同步最新源码&风格统一,所有截图均为 chromium 里最新实时截图,就不看我本地代码了。
v8源码宏观铺垫与分析
我会先为大家讲解一下各种铺垫知识,来慢慢揭开v8这神秘魔法石的一部分 -- js Context&Scope&others
。
注意下文中我画的图、翻译的话、我打的注释希望XDJMM可以一个字一个字的扫一遍,很重要的,我长时间的摸索才得来的经验。不读一下的话,我担心后文你们直接懵了
Handle--大智若愚的"憨豆"先生
首先:Handle用于管理v8对象的内存地址,它的本质就是一个指向指针的指针。
重要的前菜--指针介绍
C/C 之所以那么汹涌澎湃、大海无量,我个人认为指针(针哥)占了50%的功劳。
我们的程序、变量等,最终一定是存到物理硬件中的。运行程序一般都是在内存中,分配的空间也在内存中。你要找到某个东西,就一定需要地址,C/C 是偏底层的语言,因为它们能通过指针直接操作内存单元!
指针就是地址,地址就是指针。地址就是内存单元的编号。指针变量就是就是一个存放指针/地址的变量。申请内存空间后的返回的地址都是内存单元的起始地址,然后通过偏移量即可访问具体数据。
指针的几点好处:可以访问硬件、快速的传递数据(通过指针可以直接找到一个复杂的数据结构的起始地址)..
那么为何需要"憨豆"先生呢?
首先Handle应取其句柄含义,起源于Handle-C。我觉得你可以直接理解为柄,联想到什么了吗?手提袋的柄?打魂斗罗的游戏机手柄?书包上面的柄?无所谓,差不多就那个意思,啥意思?有点拿捏了&&四两拨千斤的韵味。
v8中重新实现了一个Handle类,然后我们先来看源码中的一段注释,看看为啥需要"憨豆"先生:
从 v8 返回的所有对象都必须由垃圾收集器跟踪,为了知道它们是否还活着。又因为垃圾收集器可能会移动对象,直接指向一个对象是不安全的。相反,所有对象都存储在垃圾收集器所知道的句柄中,并在对象移动时更新句柄。Handles应该总是按值传递(地址也是值,一般用16进制)(除了像out-parameters这样的情况--我并不知道这句个out-parameters是指什么,不过对本文应该没影响),并且它们不应该在堆上分配。
这有两种类型的句柄,local 和 persistent 类型的句柄。Local类型的句柄是轻量且瞬态的,通常用于local(我理解应翻译为:局部)操作。它们都由HandleScope管理。这意味着当Handles们被创建在有效的HandleScope内部时,一定是位于栈上。要将local句柄传递给外部 HandleScope,必须使用 EscapableHandleScope 及其 Escape() 方法。
本文涉及的源码涉及的代码里都是 Local 类型句柄,先不用管 Persistent。
当存储对象跨越多个独立操作时,可以使用持久句柄,并且在不再使用时必须明确释放。通过取消引用句柄来提取存储在句柄中的对象是安全的(例如,从Local中提取Object*),该值仍将由幕后的句柄控制,并且相同的规则适用于这些值的句柄。
Type* temp; 这代表的是一个叫temp的变量, 持有一个指向Type类型的指针;temp就是那个指针变量!通过 (*temp) 就可以拿到那个指针/地址所在的内存单元上存储的数据!但是这么做比较危险!因为GC可能移动对象从而导致产生新地址,原地址将会存着啥,谁都不知道。就是说,如果temp指向一个对象,GC换了地址存储这个对象,那开发者拿到的原地址上是啥,谁都不知道了。难道我们开发者不要面子的吗??!!所以得蹦出个Handle,来帮你管理这个对象的地址,你拿着Handle就行。你可以理解为Handle是一个指向指针的指针;一个智能的指针。