前端枚举这样玩,效率超高🚀!就是容易被打

23,503 阅读3分钟

偶然间,我发现了一种前端枚举 的新玩法。起初一脸懵逼,心想:“这是啥?”

缓过神来,直呼“精妙!牛哔!效率贼高!”

尝试在公司的项目里用了用,确实感觉程序的执行效率变快了(当然,肯定有心理暗示)。

当周 Code Review 时,同事们惊了:

让我们看看就是怎样的一种玩法!

一、枚举定义

/**
 * SKILLS:面试者的技能枚举
 **/
const SKILLS = {
  CSS: 1 ,
  JS: 1 << 1,
  HTML: 1 << 2,
  WEB_GL: 1 << 3
}

枚举的定义看起来平平无奇,但又有些奇怪,是吗?

解释下:

<< 符号是 js 中的位操作符,1 << N 的意思是:将数字 1 的位向左移动 N

1 << 1 // 2
// 二进制:1 => 10
1 << 2 // 4
// 二进制:10 => 100
...
1 << N // 2的N次方

二、枚举使用

我们按照 “步骤一” 里的方式定义了枚举,那应该怎么使用呢?

假设一个场景:当我们面试一个前端同学时,我们会不断标记他有哪些技能,这个添加标记的过程如下:

let skills = 0
// 增加一项他会的技能
function addSkill(skill) {
  skills = skills | skill // 加上
}

addSkill(SKILLS.CSS) // 1
addSkill(SKILLS.HTML) // 5
addSkill(SKILLS.WEB_GL) // 13

完成了技能的添加,我们就需要在其他地方开销这个人具备什么技能了,开销过程如下:

// 判断他是否会 CSS
SKILLS.CSS & skills // 1 (0 代表不会,非0代表会)

// 判断他是否会 JS
SKILLS.JS & skills // 0 (0 代表不会,非0代表会)


// 判断他是否会 HTML 且会 WebGl
SKILLS.HTML & skills && SKILLS.WEB_GL & skills // 8 (0 代表不会,非0代表会)

// 判断他是否会 JS 或会 HTML
SKILLS.JS & skills || SKILLS.HTML & skills

OK!基本能力具备了。

有同学要问了:“它有什么优点?可读性这么差!”

当然有优点,优点就在于:它效率 贼高

三、效率对比

为了验证它的“效率有多高”,我写了两个常见的“对照组”写法来进行验证。

常见写法一:数组式玩法

let skills = []
function addSkill(skill) {
  if (!skills.includes(skill)) { // 判断技能术里是否有该技能
    skills.push(skill)
  }
}

addSkill(SKILLS.CSS) // 1
addSkill(SKILLS.HTML) // 5
addSkill(SKILLS.WEB_GL) // 13

skills.includes(SKILLS.CSS)
skills.includes(SKILLS.JS)
skills.includes(SKILLS.HTML) && skills.includes(SKILLS.WEB_GL)
skills.includes(SKILLS.JS) || skills.includes(SKILLS.HTML)

这种写法,使用一个数组来存储技能枚举,再通过 arr.includes() 方法来判断枚举是否已被存储。

常见写法二:Map 式玩法

let skills = {}
function addSkill(skill) {
  if (!(skills[skill])) { // 判断技能术里是否有该技能
    skills[skill] = true
  }
}
addSkill(SKILLS.CSS) // 1
addSkill(SKILLS.HTML) // 5
addSkill(SKILLS.WEB_GL) // 13

skills[SKILLS.CSS]
skills[SKILLS.JS]
skills[SKILLS.HTML] && skills[SKILLS.WEB_GL]
skills[SKILLS.JS] || skills[SKILLS.HTML]

这种方法则是通过 { [value]: true } 的方式来存储枚举,然后再通过 map[value] 的方式进行取值,来判断枚举是否已被存储。

对比结果

猜猜看,三种方式的效率排名会怎样?
结果如下:
最快组:位运算组(本文推荐的玩法)

每秒执行 11 亿次

第二名:数组式玩法

每秒执行 3124 万次

最慢组:Map 式玩法

每秒执行 952 万次

用例地址: jsbench.me/4okyr97qi9/…

感兴趣的朋友可访问上面的链接进行自行验证。

按结果分析,最快最慢 之间甚至有 2-3 个数据级的差别。

四、原理分析(为啥这么快!)

本文推荐的“位运算式”为什么效率这么高?它又是怎么通过位运算完成上述内容的功能的?

4.1 定义原理

这得从 Javascript 中整型的数据格式说起,有符号整数使用 31 位表示整数的数值,用第 32 位表示整数的符号,0 表示正数,1 表示负数。数值范围为 [-2^31,2^31-1] ,即[ -2147483648, 2147483647]。 如图所示 当我们定义枚举时,每个枚举都占领着不同位上的一个二进制值。
因此,这种方式仅仅适合少于等于31个枚举项 的场景。

4.2 存值原理

为什么 skills = skills | skill 这串代码能表示 skills 中增加了一个枚举项
|Javascript 中的位运算符:
当两个数值进行 | 运算时,会对每一位进行比较,任何一位上,如果两个数都为 0,则结果为 0;其余情况结果为 1;
例如:

1 << 1 | 1 << 2 // 010 | 100
// 结果为 110,即数字:6

1 | 1 << 3 // 0001 | 1000
// 结果为 1001,即数字:9

因此,只要执行过 skills = skills | skill ,其结果中,skill 对应的那一位上一定存在一个 1

4.3 取值原理

为什么 SKILLS.CSS & skills 返回 非0 值时,可以代表 skills 上包含 SKILLS.CSS?反之则不包含?
&Javascript 中的运算符: 。 当两个数值进行 & 运算时,会对每一位进行比较,任何一位上如果两个数都为 1,则结果为 1;其余情况结果为 0;
例如:

1 & 9 // 0001 & 1001 = 0001 (1)

2 & 8 // 0010 & 1000 = 0000 (0)

因此,SKILLS.CSS & skills 返回 非0 值时,就能判定 skills 中一定存在 SKILLS.CSS

五、谁在用?(尤雨溪:正是在下)

以上用法我是从哪里看到的呢?
没错,正是从 vue3 源码中看到的。 源码地址:github1s.com/vuejs/core/…

在该源码中,vue3 通过此种方式定义枚举,开销枚举,非常值得我们学习。
ShapeFlags 的枚举定义方式:

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

结束

我是春哥
我热爱 vue.js , ElementUI , Element Plus 相关技术栈,我的目标是给大家分享最实用、最有用的知识点,希望大家都可以早早下班,并可以飞速完成工作,淡定摸鱼🐟。

你可以在掘金关注我:春哥的梦想是摸鱼,也可以在公众号里找到我:前端要摸鱼
希望大家在 2022 变得更强。