怎么看源码的代码实现_从Chrome源码看JS Array的实现

本文探讨了JavaScript数组的实现,包括快速和慢速元素模式,以及数组操作如push、pop、shift、unshift和splice的内部机制。文章指出,数组在不同场景下的性能表现,比如push用汇编实现以提高速度,而中间插入元素可能导致效率下降。此外,通过与线性链接结构的比较,突显了数组在不同操作下的优劣。最后,文章分析了join和sort的实现,以及快速排序算法的选择枢轴元素策略。
摘要由CSDN通过智能技术生成

b1bc6e00f5abeb94ba88a3e5c32621c1.png

我们在上一篇介绍了JS Object的实现,这一篇将进一步介绍JS Array的实现。

在此之前,笔者将Chromium升级到了最新版本60,上一次是在元旦的时候下的57,而当前最新发布的稳定版本是57。57是三月上旬发布的,所以Chrome发布一个大版本至少用了两、三个月的时间。Chrome 60的devTool增加了很多有趣的功能,这里顺便提一下:

ce9b225f72b90cc52def6617a2b76e08.png

例如把没有用到的CSS/JS按比例标红,增加了全页的截屏功能,和一个本地代码的编辑器:

a7575adb7d981e6c0d2a73c27656d9bb.png

回到正文。

JS的Array是一个万能的数据结构,为什么这么说呢?因为首先它可以当作一个普通的数组来使用,即通过下标找到数组的元素:

var 

然后它可以当作一个栈来使用,我们知道栈的特点是先进后出,栈的基本操作是出栈和入栈:

var 

同时它还可以当作一个队列,队列的特点是先进先出,基本操作是出队和入队:

var 

甚至它还可以当作一个哈希表来使用(但是不推荐这么用):

var 

另外,它还可以随时随地增删数组中任意位置的元素:

var 

JS Array一方面提供了很大的便利,只要用一个数据结构就可以做很多事情,使用者不需要关心各者的区别,使得JS很容易入门。另一方面它屏蔽了数据结构的概念,不少写前端的都不知道什么是栈、队列、哈希、树,特别是那些不是学计算机,中途转过来的。然而这往往是不可取的。

另外一点是,即使是一些前端的老司机,他们也很难说清楚,这些数组函数操作的效率怎么样,例如说随意地往数组中间增加一个元素不会有性能问题么。所以就很有必要从源码的角度看一下数组是怎么实现的。

1. JS Array的实现

先看源码注释:

// The JSArray describes JavaScript Arrays

这里说明一下,如果不熟悉C/C++的,那把它成伪码就好了。

源码里面说了,JSArray有两种模式,一种是快速的,一种是慢速的,快速的用的是索引直接定位,慢速的使用用哈希查找,这个在上一篇《从Chrome源码看JS Object的实现》就已经提及,由于JSArray是继承于JSObject,所以它也是同样的处理方式,如下面的:

var 

增加一个2000的索引时,array就会被转成慢元素。

如下的数组:

var 

把a打印出来:

- map = 0x939ebe04359 [FastProperties]
- prototype = 0x27e86e126289
- elements = 0xe70c791d4e9 <FixedArray[3]> [ FAST_SMI_ELEMENTS (COW)]
- length = 3
- properties = 0x2b609d202241 <FixedArray[0]> { #length: 0x2019c3e58da9 <AccessorInfo> (const accessor descriptor)
}
- elements= 0xe70c791d4e9 <FixedArray[3]> {
0: 8
1: 1
2: 2
}

它有一个length的属性,它的elements有3个元素,按索引排列。当给它加一个2000的索引时:

var 

打印出来的array变成:

- map = 0x333c83f9dbb9 [FastProperties]
- prototype = 0xdcc53ba6289
- elements = 0x21a208a1d541 <FixedArray[29]> [ DICTIONARY_ELEMENTS]
- length = 2001
- properties = 0x885d1402241 <FixedArray[0]> {
#length: 0x1f564a958da9 <AccessorInfo> (const accessor descriptor)
}
- elements= 0x21a208a1d541 <FixedArray[29]> {
2: 2 (data, dict_index: 0, attrs: [WEC])
0: 8 (data, dict_index: 0, attrs: [WEC])
2000: 10 (data, dict_index: 0, attrs: [WEC])
1: 1 (data, dict_index: 0, attrs: [WEC])
}

elements变成了一个慢元素哈希表,哈希表的容量为29。

由于快元素和慢元素上一节已经有详细讨论,这一节将不再重复。我们重点讨论数组的操作函数的实现。

2. Push和扩容

数组初始化大小为4:

// Number of element slots to pre-allocate for an empty array.

执行push的时候会在数组的末尾添加新的元素,而一旦空间不足时,将进行扩容。

在源码里面push是用汇编实现的,在C++里面嵌入的汇编。这个应该是考虑到push是一个最为常用的操作,所以用汇编实现提高执行速度。在汇编的上面封装了一层,用C++调的封装的汇编的函数,在编译组装的时候,将把这些C++代码转成汇编代码。

计算新容量的函数:

Node

如上代码新容量等于 :

new_capacity = old_capacity /2 + old_capacity + 16

即老的容量的1.5倍加上16。初始化为4个,当push第5个的时候,容量将会变成:

new_capacity = 4 / 2 + 4 + 16 = 22

接着申请一块这么大的内存,把老的数据拷过去:

Node

由于复制是用的memcopy,把整一段内存空间拷贝过去,所以这个操作还是比较快的。

再把新元素放到当前length的位置,再把length增加1:

StoreFixedArrayElement

可以来改点代码玩玩,我们知道push执行后的返回结果是新数组的长度,尝试把它改成返回老数组的长度:

Node 

重新编译Chrome,在控制台上执行比较如下:

03f034775e8ff1235bfe87373079624f.png

右边的新Chrome返回了4,左边正常的Chrome返回5.

3. Pop和减容

push是用汇编实现,而pop的逻辑是用C++写的。在执行pop的时候,第一步,获取到当前的length,用这个length - 1得到要删除的元素,然后调用setLength调整容量,最后返回删除的元素:

int 

我们重点看下这个减容的过程:

if 

如果容量大于等于length的2倍,则进行容量调整,否则用holes对象填充。第三行的rightTrim函数,会算出需要释放的空间大小,并做标记,并等待GC回收:

int 

也就是说,当数组的元素个数小于容量的一半时,就会进行减少的操作,将容量调整为实际的大小。

4. shift和splice数组中间的操作

push和pop都是在数组末尾操作,相对比较简单,而shfit、unshfit、splice是在数组的开始或者中间进行操纵。我们来看一下,如果是这种情况的又是如何调整数组元素的。

(1)shift是出队,即删除并返回数组的第一个元素。shift和pop调的都是同样的删除函数,只不过shift传的删除的postion是AT_STRT,源码里面会判断如果是AT_START的话,会把元素进行移动:

if 

从1的位置移到0的位置,如上面第2行的第4、5个参数,这个move将会调leftTrim,和上面的rightTrim相反:

*

(2)unshfit在数组的开始位置插入元素,首先要判断容量是否足够存放,如果不够,将容量扩展为老容量的1.5倍加16,然后把老元素移到新的内存空间偏移为unshift元素个数的位置,也就是说要腾出起始的空间放unshfit传进来的元素,如果空间足够了,则直接执行memmove移动内存空间,最后再把unshif传进来的参数copy到开始的位置:

int 

并更新array的length。

(3)splice的操作已经几乎不用去看源码了,通过shift和unshift的操作是怎么样的,就可以想象到它的执行过程是怎样的,只是shift/unshfit操作的index是0,而splice可以指定index。具体代码如下:

// Delete and move elements to make space for add_count new elements.

它需要先shrink或者grow中间元素的空间,以适应增加元素比删除元素少或者多的情况,然后进行容量调整和移动元素。

接着再来看下两个“小清新”的函数

5. Join和Sort

说它们是小清新,是因为它们是用JS实现的,然后再用wasm打包成native code。不过,join的实现逻辑并不简单,因为array的元素本身具有多样化,可能为慢元素或者快元素,还可能带有循环引用,对于慢元素,需要先排下序:

var 

预处理完之后,最后创建一个字符串数组,用连接符连起来:

// Construct an array for the elements.

而sort函数是用的快速排序:

function 

当数组元素的个数不超过10个时,是用的插入排序:

function 

快速排序算法里面有一个比较重要的地方是选择枢纽元素,最简单的是每次都是选取第一个元素,或者中间的元素,在源码里面是这样选择的:

if 

如果元素个数在1000以内,则使用它们的中间元素,否则要算一下, 这个算法比较有趣:

function 

先取一个递增间距200~215之间,再循环取出原元素里面落到这个间距的元素,放到一个新的数组里面(这个数组是C++里面的数组),然后排下序,取中间的元素。因为枢纽元素的刚好是所有元素的中位数时,排序的效果最好,而这里是取出少数元素的中位数,类似于抽样模拟,缺点是它得再借助另外的排序算法。

最后再比较一下Array和线性链接的速度。

6. Array和线性链接的速度

线性链接是一种非连续存储的数据结构,每个元素都有一个指针指向它的下一个元素,所以它删除元素的时候不需要移动其它元素,也不需要考虑扩容的事情,但是它的查找比较慢。我们实现一个简单的List和Array进行比较。

List的每个节点用一个Node表示:

class 

每个List都有一个头指针指向第一个元素,和一个length记录它的长度:

class 

然后实现它的push和unshift函数:

class 

两个函数都会调一个通用的insert函数:

insert

有了这个List之后,就可以初始化一个list和array:

var 

可以来比较这个List和Array的存储方式,非连续和连续的区别:

3586db7ae1ca49b85044c3613c88dd6d.png

然后用下面的代码比较List和Array在数组起始位置插入元素的操作时间:

var 

再比较从正中间位置插入元素的时间:

console

运行可以得到以下表格:

b9fa4961f01ca2c0e359002452ecc8aa.png

可以看到在队首插入元素,使用线性链接List的时间将会数量级的优于Array。如果是在中间位置插入的话,由于 List的查找花费了很多时间,导致总时间明显高于Array。但是如果在插入的时候,记住上一次的位置,那么List又会明显快于Array。如下换成记录插入的位置:

console

时间比较List又快于Array:

d07dcf7401b7ce7bbb80ab7cad43c57f.png

综上,本文介绍了JS Array的实现,特别是它的操作函数,分析了它是怎么调整容量和移动元素的,并用了一个线性链接进行比较。Array的实现用了三种语言:汇编、C++和JS,最常用的如push用了汇编实现,比较常用的如pop/splice等用了C++,较为少用的如join/sort用了JS。

Array为快元素即普通的数组时,增删元素操作需要不断的扩容、减容和调整元素的位置。特别是当不断地在起始位置插入元素时,和链表相比,这种时间效率还是比较低下的。如果使用的场景是要根据index删除元素,使用Array还是有优势,但是若能够很快定位到删除元素的位置,链表毫无疑问是更合适的。

相关阅读:

  1. 从Chrome源码看浏览器如何构建DOM树
  2. 从Chrome源码看浏览器的事件机制
  3. 从Chrome源码看浏览器如何计算CSS
  4. 从Chrome源码看浏览器如何layout布局
  5. 从Chrome源码看JS Object的实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值