本篇文章参考以下博文
文章目录
前言
最近在学习 Deno 的时候,发现原生支持 WebAssembly 类型 ,第一眼接触的时候有点陌生,查了一下资料后浑身冷汗。
简单来说,这东西可以让 C,C++ 的开发或者一些其他语言的开发,编写的代码运行在浏览器上,完事运行效率还比 JavaScript 高,而且更轻量级。据说执行速度比 JS 高 10倍 !
各位前端开发,为了不丢饭碗,含着泪也要学。
一、知己知彼
首先我们得要了解一下 JavaScript 本身的运行机制,当把编写的代码放到浏览器上的时候,我们希望浏览器可以按照代码里面的描述,操作计算机完成相关功能,由于计算机并不认识 JavaScript 所以这个时候 JS引擎 站了出来,我们熟悉的 V8 就是这样一名翻译官。
接下来一个问题就是,该怎么翻译?
一般来说,发言者说一句,翻译官就解释一句,有条不紊的进行,直到完成所有。这种模式就是 解释器 (及时生效)。
另一方式是翻译书籍,翻译官会把整本书都翻译过来之后,再进行发布,这种模式就是 编译器 ,你看到的东西,已经全部翻译好了(执行前翻译)。
每种方式都有优缺点。
1.1 解释器
解释器很快的获取代码并且执行。我们不需要执行代码的时候知道全部的编译步骤。因此,解释器与 JavaScript 有着天然的契合。 web 开发者能够立即得到反馈很重要。
这也是浏览器最开始使用 JavaScript 解释器的原因之一。
但是使用解释器的弊端是当您运行相同的代码的时候。比如,当执行了一个循环。然后就会一遍又一遍的做同样的事情。
1.2 编译器
编译器则有相反的效果。在程序开始的时候,它可能需要稍微多一点的时间来了解整个编译的步骤。但是当运行一个循环的时候他会更快,因为他不需要重复的去翻译每一次循环里的代码。
1.3 浏览器
浏览器为了提高性能,引入了编译器,不同的浏览器实现起来稍有不同,但是基本目的是相同的。他们给 JavaScript 引擎添加了一个新的部分,称为监视器(也称为分析器)。该监视器在 JavaScript 运行时监控代码,并记录代码片段运行的次数以及使用了那些数据类型。
如果相同的代码行运行了几次,这段代码被标记为 “ warm ”。如果运行次数比较多,就被标记为 “ hot ”。被标记为 “ warm ” 的代码被扔给基础编译器,只能提升一点点的速度。被标记为 “ hot ” 的代码被扔给优化编译器,速度提升的更多。
1.4 性能比较
1.4.1 解析
JavaScript 源码一旦被下载到浏览器,源将被解析为抽象语法树( AST )。
通常浏览器解析源码是懒惰的,浏览器首先会解析他们真正需要的东西,没有及时被调用的函数只会被创建成存根。
在这个过程中, AST 被转换为该 JS 引擎的中间表示(称为字节码)。
相反, WebAssembly 不需要被转换,因为它已经是字节码了。它仅仅需要被解码并确定没有任何错误。
1.4.2 编译 + 优化
如前所述, JavaScript 是在执行代码期间编译的。因为 JavaScript 是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译。这样会消耗时间。
相反, WebAssembly 与机器代码更接近。例如,类型是程序的一部分。这是速度更快的一个原因:
- 编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化。
- 编译器不需要去每次执行相同代码中数据类型是否一样。
1.4.3 垃圾回收
在 JavaScript 中,开发者不需要担心内存中无用变量的回收。 JS引擎 使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。
这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。
现在, WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像 C/C++ )。虽然这些可能让开发者编程更困难,但它的确提升了性能。
二、WebAssembly 如何工作
要了解 WebAssembly 如何工作,需要先了解计算机是如何解析和理解交流内容的,计算机可以分为三部分功能:
- 负责思考的部分是算术逻辑单元( ALU )。
- 短期储存由寄存器( Registers )提供。
- 随机存储器(或 RAM )来提供长期储存能力
机器码中的语句被称为指令。
当一条指令进入计算机时会发生什么?它被拆分成了多个的部分并有特殊的含义。
被拆分成的多个部分分别进入不同的计算机单元进行处理,这也是拆分指令所依赖的方式。
例如,这个计算机从机器码中取出 4-10 位,并将它们发送到 ALU 。 ALU 进行计算,它根据 0 和 1 的位置来确定是否需要将两个数相加。
这个块被称为“操作码”,因为它告诉 ALU 执行什么操作。
那么这个计算机会拿后面的两个块来确定他们所要操作的数。这两个块对应的是寄存器的地址。
请注意添加在机器码上面的标注( ADD R1 R2 ),这使我们更容易了解发生了什么。这就是汇编。它被称为符号机器码。这样人类也能看懂机器码的含义。
可以看到,这个机器的汇编和机器码之间有非常直接的关系。每种机器内部有不同的结构,所以每种机器都有自己独有的汇编语言。
所以我们并不只有一个翻译的目标。
相反,我们的目标是不同类型的机器码。就像人类说不同的语言一样,机器也有不同的语言。
我们希望能够将这些任何一种高级编程语言转换为任何一种汇编语言。这样做的一个方法是创建一大堆不同的翻译器,可以从任意一种语言转换成任意一种汇编语言。
这样做的效率非常低。为了解决这个问题,大多数编译器会在高级语言和汇编语言之间多加一层。编译器将把高级语言翻译成一种更低级的语言,但比机器码的等级高。这就是中间代码( IR )。
意思就是编译器可以将任何一种高级语言转换成一种中间语言。然后,编译器的另外的部分将中间语言编译成目标机器的汇编代码。
编译器的“前端”将高级编程语言转换为 IR 。编译器的“后端”将 IR 转换成目标机器的汇编代码。
三、WebAssembly 适合在哪里使用?
当你的代码运行在用户的机器的 web 平台上的时候,你不知道你的代码将会运行在那种机器结构上。
所以 WebAssembly 和别的汇编语言是有一些不同的。所以他是一个概念机上的机器语言,不是在一个真正存在的物理机上运行的机器语言。
正因如此, WebAssembly 指令有时候被称为虚拟指令。它比 JavaScript 代码更快更直接的转换成机器代码,但它们不直接和特定硬件的特定机器代码对应。
在浏览器下载 WebAssembly 后,使 WebAssembly 的迅速转换成目标机器的汇编代码。
如果想在页面里上添加 WebAssembly ,需要将代码编译成 .wasm 文件。
当前对 WebAssembly 支持最多的编译器工具链称是 LLVM 。有许多不同的“前端”和“后端”可以插入到 LLVM 中。
四、在 JavaScript 中加载一个 .wasm 组件
.wasm 文件是 WebAssembly 组件,它可以被 JavaScript 加载。到目前为止,加载过程有点复杂。
function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}
后面会慢慢优化到模块管理工具(如 Webpack )或加载器(如 SystemJS )相结合
注意: WebAssembly 模块和 JS 模块之间存在重大差异。目前 WebAssembly 中的函数只能使用 WebAssembly 类型(整数或浮点数)作为参数或返回值。对于任何更复杂的数据类型(如字符串),必须使用 WebAssembly 模块的内存。
总结
看到这里可能大家对于 WebAssembly 还是没有一个具象的认识,或者大家关心的只是这玩意会不会取代 JS ?
大家不用担心这些,这两者是可以共存的,比如 JS 就可以引入 WebAssembly 来处理一些比如动画等较为消耗性能的模块。虽然 WebAssembly 可以把让其他语言代替 JS 进行工作,但是 JS 配套的生态,快速生成页面的能力,能甩 WebAssembly 好几条街。
作为一名开发,入行那天我已经做好终身学习的准备了,当每天都能看到不同世界的时候,我会更好奇这个世界。