在某次迭代中需要给一个表格加个选择框,原型如下:
后端给的状态值如下:
javascript
1: 已停止
2:运行中
3:正在启动
4:启动失败
5:正在停止
6:已禁用
7:休眠中
8:任务发布中
然后我的代码如下所示:
javascript
statusMap: {
0: '全部服务状态',
2: '运行中',
3: '正在启动',
4: '启动失败',
5: '正在停止',
1: '已停止',
7: '休眠中',
6: '已禁用',
8: '任务发布中'
}
<v-select v-model="status">
<v-option
v-for="(value, key) in statusMap"
:key="'status' + key"
:value="key"
:label="value">
</v-option>
</v-select>
结果最终的效果图如下所示:
似乎下拉框中的选项没有按照我的代码中的顺序渲染,而是按照从小到大的顺序重排了...
什么情况?
这是因为在ECMAScript
中规定:如果是数字属性,则按照从小到大的顺序排序,如果是字符串属性,则按照创建的顺序排序。 ECMAScript2015规范
那么在v8
中,js
中的对象是怎么存储的呢?
注:下文中用到的%DebugPrint
和%DebugPrintPtr
都是v8-debug
中的命令,v8-debug
可以通过jsvu
安装,jsvu
可以通过npm
安装
我们直接通过demo
来看下
javaScript
const obj = {}
%DebugPrint(obj)
除了我们熟悉的prototype
之外,可以看到对象上有Map
、Element
、Properties
等。
Elements
在v8
中,elements
是用来存储对象的数字属性的,称为索引属性indexed properties
。 索引属性会按照索引从小到大存储在elements
中。测试一下:
javaScript
const obj = {}
obj[2] = 2
obj[0] = 0
obj[1] = 1
%DebugPrint(obj)
可以看到elements
上的索引属性已经排好序了。
properties
在v8
中,properties
是用来存储对象的字符串属性的,称为命名属性Named properties
。命名属性会按照创建时的顺序存储在properties
中。
javaScript
const obj = {}
obj.a = 'a'
obj.b = 'b'
%DebugPrint(obj)
似乎测试的结果没有符合我们的预期,可以看到图中的properties
的长度是0
,而且列举出来的命名属性a
和b
所在的位置是in-object
。
解释这个问题之前,我们先设想一个问题,v8
是怎么查找属性值的?比如现在想知道obj.a
是多少。因为命名属性保存在properties
中,所以v8
要先找到properties
,然后再在properties
中找到a
。这样无疑降低了查询效率。
为了提升性能,v8
采取了将部分命名属性直接存储在对象上的策略。这种直接存储在对象上的属性就叫in-object properties
。
通过上面的截图我们可以看到v8
预置了4
个in-object properties
的插槽。当命名属性的个数超过4
个的时候。剩下的属性会存储到properties
上。我们在上面的demo
中增加几行代码测试下:
javaScript
const obj = {}
obj.a = 'a'
obj.b = 'b'
obj.c = 'c'
obj.d = 'd'
obj.e = 'e'
obj.f = 'f'
%DebugPrint(obj)
所以通过上面的测试,我们可以得出如下结论:
- 索引属性和命名属性是分开存储的
- 为了提升访问效率,对象本身会预留
4
个in-object
位置存放命名属性,剩下的命名属性会存储到properties
上
在js
中,我们创建对象一般有两种方式,一种是通过上面例子中用到的字面量的方式创建,还有一种是通过构造函数的方式创建。那么当用构造函数的方式创建对象的时候,上面的结论还存在吗?测试一下:
javaScript
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 12; i++) {
foo[i] = i
foo['p' + i] = 'p' + i
}
%DebugPrint(foo)
通过上图可以看出,上面的结论还是成立的。不同点就是in-object
的个数变成10
个了。还有一个小细节就是elements
的初始长度和扩容的规则不一样,不过这又是另外一个话题了,略过。
快属性和慢属性
先看两个例子:
javaScript
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 12; i++) {
foo['p' + 1] = 'p' + 1
}
%DebugPrint(foo)
javaScript
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 26; i++) {
foo['p' + 1] = 'p' + 1
}
%DebugPrint(foo)
通过上面两张图可以看到,properties
的数据结构不一样。当属性个数是12
的时候,properties
的数据结构是数组,而当属性个数是26
的时候,数据结构变成字典了。这也就是我们常说的快属性和慢属性。
那什么是快慢属性呢?我们将保存在线性结构中的属性称为快属性 。因为访问速度快,通过偏移量就可以访问到。但是如果要对线性结构进行增加或者删除操作,那么效率则会低下。所以在属性过多的时候(经测试,大于25
个),v8
会采用非线性结构来存储属性,保存在非线性结构中的属性就称为慢属性。
在日常编码中,我们经常使用的delete
操作(非末尾元素)也会导致对象的属性从快属性变成慢属性。
javaScript
function Foo() {};
const foo = new Foo();
for (let i = 0; i < 12; i++) {
foo['p' + i] = 'p' + i;
}
delete foo.p1;
foo.p1 = 'p1';
%DebugPrint(foo);
关于v8
中的快属性的更多细节可以阅读Fast properties in V8
既然properties
有快属性之分,那么elements
呢?那肯定也是有的。在上面所有的例子中elements
的数据结构都是FixedArray
。也就是线性存储的。那什么情况下elements
会变成非线性存储呢?
在回答这个问题之前,我们先看下一个概念HOLEY_ELEMENTS
。 还是先来两个demo
吧
javaScript
const arr1 = ['a', 'b', 'c'];
const arr2 = ['a', 'b', 'c'];
arr2[5] = 'e';
%DebugPrint(arr1);
%DebugPrint(arr2);
这样一看是不是比较好理解,如果数组中有元素等于the_hole_value
(也就是我们说的empty
),那么就是HOLEY_ElEMENTS
(稀疏数组),如果数组中没有empty
元素,则是PACKED_ELEMENTS
(密集数组)。根据元素值的类型还可以细分出其它种类的HOLEY_ELEMENTS
,比如PACKED_SMI_ELEMENTS
(元素的值都是整数)等,PACKED_ELEMENTS
同理。但是和本文主题无关,同样略过,对此感兴趣的可以查看Elements kinds in V8
现在回到上面那个问题,什么时候elements
会非线性存储呢?和properties
类似,当elements
有大量(经测试,大量是指不小于1024
)hole
的时候。elements
会变成非线性结构。可以通过下面的demo
验证下。
javaScript
const obj = {};
obj[1023] = 0;
%DebugPrint(obj);
javaScript
const obj = {};
obj[1023] = 0;
%DebugPrint(obj);
javaScript
// 测试下是不是有hole的元素个数>=1024个从线性变非线性
const obj = {};
obj[1] = 0;
obj[1023] = 0;
%DebugPrint(obj);
tips
: 所谓的快慢属性只是我们的习惯叫法,并没有优劣之分,它们有各自使用的场景,搬运v8
某个开发者的一段话:
更多细节可以点击这里
Map
什么是隐藏类
在v8
中,每个对象的第一个属性就是自己的隐藏类map
。其实在之前的例子中我们已经见过隐藏类了,就是下图红色框框的内容。(注意下蓝色框框的内容instance descriptors
,这是隐藏类中一个比较重要的属性)
为什么需要隐藏类
先来几行代码
javaScript
const obj = {
name: 'lily',
age: 10
}
console.log(obj.name)
当执行obj.name
的时候,v8
会怎么查找?因为javaScript
是动态语言,也就是说对象的属性是可以随意增减的,所以当执行obj.name
的时候,v8
连obj
上有没有name
都不知道,就不要说obj.name
的值了。为了找到obj.name
的属性值,v8
需要先在in-object properties
上查找是否有name
,没找到则去properties
上找,还没找到则需要顺着原型链去找。可以看到这整个的过程是很耗时的。追求高性能的v8
肯定不会用这样的方式查找对象的属性值。所以就借鉴了静态语言的类和结构体的概念引入了隐藏类的策略。下面我们来看下隐藏类是怎么提升查找对象属性值的?
还是上面的obj
。我们先用%DebugPrint(obj)
看下obj
的map
地址,然后查看下map
上的instance descriptors
中的详细内容:
有了map
之后,当执行obj.name
的时候,v8
会直接在obj
的map
的instance descriptors
中找到name
属性相对于obj
的偏移量,根据obj
的内存地址加上name
的偏移量0
就得到了name
属性在内存中的地址了,此时就可以直接取值了,是不是快多了?
可以这么说:静态语言根据类或者结构体生成对象,而v8
根据对象生成隐藏类。
但是如果每个对象都产生一个隐藏类的话,那么时间和内存开销也是两个大问题,所以就出现复用隐藏类了,那么什么情况下会复用隐藏类呢?要符合三个条件:属性的名称、顺序和个数要完全相同 。用下面的demo
测试下:
javaScript
const obj1 = { name: '小红', age: 10 };
const obj2 = { name: '小米', age: 11 };
const obj3 = { name: '小花' };
const obj4 = { age: 10, name: '小草' };
%DebugPrint(obj1);
%DebugPrint(obj2);
%DebugPrint(obj3);
%DebugPrint(obj4);
从截图可以看到,只有obj1
和obj2
的map
是同一个。
v8
引入隐藏类是基于一个假设:对象创建出来形状就固定了,即属性不会新增或者删除。但事实是js
是动态语言,对象的属性可以随意增减,那现在我们来看下对象属性增减的时候隐藏类如何变化?
注意下面这个demo
截图中map
和back pointer
的变化~
javaScript
// 先初始化一个obj
const obj = {}
javaScript
obj.name = '小红'
javaScript
obj.age = 10
从上面三张图可以得出的结论就是:对象每添加一个新属性,就会产生一个新的隐藏类,而且新的隐藏类中的 back pointer
总是指向旧的隐藏类。上面例子中三个隐藏类(按照出现的先后顺序分别命名为map0
、map1
和map2
)的关系如下图所示:
其实上面的链条就是v8
中的转换树transition tree
。它的作用是当以相同的顺序添加相同的属性的时候能确保最后得到相同的隐藏类。来测试一下:
新加一行代码obj1 = {}
,如下图所示:
要给obj1
来个name
属性呢?
但是此时给obj1
来个不一样的属性呢?比如phone
此时可以隐藏类之间的转换树如下:
现在来看看删除属性的情况,为了方便观察删除属性时map
如何变化,我们先给obj
增加一个height
属性。
javaScript
obj.height = 150
运行到上面这行代码的时候,此时内存中的各对象之间的关系如下所示:
好了,开始删除了,我们先删除最后一个属性height
发现了什么?obj
的map
退回到了0x02410015ad75
上了
再删一个看看,这次我们删除非末尾元素 name
是不是发生了点不一样的东西?是的,如果删除的属性不是最后一个属性的话,那么v8
会不再维护map
之间的转化关系,而是转成非线性结构存储了。
日常编码建议
- 初始化属性相同的对象时,保证属性的顺序保持一致
- 尽量一次性初始化完属性对象
- 避免使用
delete
说完了v8
中的对象表达,现在回到最开始的那个问题,那怎么还原原型上的顺序:
javascript
<v-select v-model="status">
<v-option label="全部服务状态" :value="0"></v-option>
<v-option label="运行中" :value="2"></v-option>
<v-option label="正在启动" :value="3"></v-option>
<v-option label="启动失败" :value="4"></v-option>
<v-option label="正在停止" :value="5"></v-option>
<v-option label="已停止" :value="1"></v-option>
<v-option label="休眠中" :value="7"></v-option>
<v-option label="已禁用" :value="6"></v-option>
<v-option label="任务发布中" :value="8"></v-option>
</v-select>
javaScript
statusMap: new Map([
[0, '全部服务状态'],
[2, '运行中'],
[3, '正在启动'],
[4, '启动失败'],
[5, '正在停止'],
[1, '已停止'],
[7, '休眠中'],
[6, '已禁用'],
[8, '任务发布中']
]),
<v-select v-model="status">
<v-option
v-for="[key, value] in statusMap"
:key="'status' + key"
:value="key"
:label="value">
</v-option>
</v-select>