开始使用 WebAssembly。
J**Ascript 自成立以来已成为最流行的编程语言,这是由 Web 的发展推动的。 Web 应用程序变得越来越复杂,但这也暴露了 j**ascript 的问题:
语法过于灵活,难以开发大型 Web 项目; 性能无法满足某些场景的需求。 针对上述两个缺点,近年来出现了一些JS的替代语言,例如:
Microsoft 的 TypeScript 通过向 JS 添加静态类型检查来改进 JS 的松散语法,并提高了健壮性; 谷歌的 dart 为浏览器引入了一个新的虚拟机,可以直接运行 dart 程序以提高性能; Firefox 的 ASMJS 是 JS 的一个子集,JS 引擎针对的是 ASMJS 用于性能优化。 上述每种尝试都有其自身的优点和缺点,其中:
TypeScript 只解决了 JS 语法松散的问题,最后需要编译成 JS 才能运行,这并不能提高性能; Dart 只能在 Chrome 预览版中运行,没有主流浏览器支持,所以使用 Dart 开发的人并不多; asm.JS 语法太简单,限制性很强,效率低下。 三大浏览器巨头纷纷提出各自的解决方案,彼此不兼容,这与网络的宗旨背道而驰; 正是技术规范的统一使网络走到了今天,因此迫切需要形成一套新的规范来解决JS面临的问题。
结果是 WebAssembly,一种新的字节码格式,已经被主流浏览器支持。 与需要解释和执行的 JS 不同,WebAssembly 字节码与底层机器代码非常相似,可以快速加载和运行,因此与 JS 解释和执行相比,性能得到了极大的提升。 也就是说,WebAssembly 不是一种编程语言,而是一种字节码标准,需要用高级编程语言编译并放入 WebAssembly 虚拟机中才能运行。
要了解 WebAssembly 是如何工作的,你需要了解计算机是如何工作的。 电子计算机是由电子元器件组成的,为了方便电子元器件的加工只存在于两种状态,分别对应0和1,也就是说计算机只知道0和1,数据和逻辑需要用0和1来表示,即可以直接加载到计算机中运行的机器码。 机器代码极不可读,所以人们用C、C++、Rust、Go等高级语言编写,然后编译成机器码。
由于不同电脑的CPU架构不同,机器码标准也不同,常见的CPU架构有x86、AMD64、ARM,所以在用高级编程语言编译时需要指定目标架构才能自给自足**。
WebAssembly 字节码是一种平滑出不同 CPU 架构的机器码,WebAssembly 字节码不能直接运行在任何一个 CPU 架构上,但因为非常接近机器码,所以可以非常快速地转换为相应架构的机器码,所以 WebAssembly 以接近机器码的速度运行, 这听起来与 J**A 字节码非常相似。
与 JS 相比,Webassembly 具有以下优点:
体积小:由于浏览器运行时只加载编译好的字节码,所以同样的逻辑比描述为字符串的JS文件要小得多; 快速加载:由于文件大小小,不需要用解释来执行,WebAssembly 可以更快地加载和实例化,减少运行前的等待时间; 兼容性问题少:WebAssembly 是一个非常低级的字节码规范,在制定之后很少改变,即使后来改变,也只需要在从高级语言编译字节码的过程中兼容即可。 可能出现兼容性问题的地方是在 JS 和 WebAssembly 之间桥接的 JS 接口中。 每种高级语言实现源代码到不同平台的机器代码的转换都是重复的,高级语言只需要生成底层虚拟机(llvm)知道的中间语言(llvm ir),llvm就可以实现:llvm ir到不同CPU架构的机器码生成; 机器代码编译时性能和大小优化。 此外,LLVM 还实现了 LLVM IR 到 WebAssembly 字节码的编译功能,也就是说,只要能将高级语言转换为 LLVM IR,就可以编译成 WebAssembly 字节码,可以编译成 WebAssembly 字节码的高级语言有:
AssemblyScript:语法与TypeScript相同,前端学习成本低,是前端编写WebAssembly的最佳选择; c C++:官方推荐的方法,详细使用详见文档; rust:语法复杂且学习成本高昂,这可能会让前端感到不舒服。 有关详细使用,请参阅文档; kotlin:语法与j**a和js类似,语言学习成本低,详细使用说明;golang 说:语法简单,学习成本低。 但是,对 WebAssembly 的支持仍处于正式发布阶段,如何使用它的详细信息可以在文档中找到。 通常,负责将高级语言翻译成 LLVM IR 的部分称为编译器前端,将 LLVM IR 编译成每个架构 CPU 对应的机器代码的部分称为编译器后端。 如今,越来越多的高级编程语言选择LLVM作为后端,而高级语言只需要专注于如何提供更高效的语法,同时保持翻译到LLVM IR的程序的性能。
让我们仔细看看如何使用 AssemblyScript 编写 WebAssembly 来计算斐波那契数列。 使用 Typescript 实现斐波那契数列计算的模块 f.TS如下:
export function f(x: i32): i32 return f(x - 1) +f(x - 2)}
按照 AssemblyScript 提供的安装教程安装成功后,再一遍asc f.ts -o f.wasm
您可以将上述内容编译成一个有效的 webassembly 模块。
为了加载和执行编译后的 fwasm 模块需要由 js 加载并调用到模块上的 f 函数中,需要以下 js **
fetch('f.wasm') 网络负载 fWASM 文件then(res => res.arraybuffer()) 到 arraybufferthen(webassembly.instantiate)编译为机器码+当前CPU架构的实例化then(mod => )
一个新的内置类型是 I32,它是 TypeScript 之上的 AssemblyScript 的内置类型。 AssemblyScript 和 TypeScript 之间略有不同,AssemblyScript 是 TypeScript 的一个子集,为了方便编译成 WebAssembly,在 TypeScript 的基础上增加了更严格的类型限制,区别如下:有比 TypeScript 更详细的内置类型来优化性能和内存使用,参见文档; 不能使用任何类型和未定义的类型,也不能使用枚举类型; 可为 null 的变量必须是引用类型,而不是基本数据类型,例如字符串、数字、布尔值; 函数中的可选参数必须提供默认值,函数必须具有返回类型,没有返回值的函数的返回类型需要为 void; 不能使用 JS 环境中的内置函数,只能使用 AssemblyScript 提供的内置函数。 一般来说,assemblyscript 比 typescript 有更多的限制,写起来会感觉非常有限; 使用 AssemblyScript 编写 WebAssembly 通常会导致 TSC 编译通过但运行 WebAssembly 错误,这可能是由于您未能遵守上述限制; 但是,AssemblyScript 可以通过修改 TypeScript 编译器的默认配置来查找生成阶段的大多数错误。
AssemblyScript 的实现原理其实是用了 LLVM,它通过 TypeScript 编译器将 TS 源码解析成 AST,将 AST 转换为 IR,然后通过 LLVM 编译成 WebAssembly 字节码。 上述限制旨在促进 AST 到 LLVM IR 的转换。
相较于C、Rust等语言编写WebAssembly,AssemblyScript的优点是前端没有额外的新语言学习成本,而且对于不支持WebAssembly的浏览器来说,可以通过TypeScript编译器编译成一个普通的JS**,从而实现从JS到WebAssembly的平滑迁移。
为了提供流畅的 WebAssembly 开发过程,我们采取以下步骤来集成 WebPack。
安装以下依赖,以便 TS 源代码通过 AssemblyScript 编译到 WebAssembly 中。
修改 webpackconfig.js,添加 loader:
module.exports = }
修改 TypeScript 编译器配置 tsconfigJSON,使 TypeScript 编译器能够支持 AssemblyScript 中引入的内置类型和函数。
该配置直接继承自 AssemblyScript 中内置的配置文件。
如前所述,WebAssembly 的二进制文件格式 wasm,这是人眼无法读取的,还有一种文本格式叫做 wast,用于读取 WebAssembly 文件的逻辑。 以前面提到的计算斐波那契数列的模块为例,对应的 wast 文件如下:
func $src/asm/module/f (param f64) (result f64)(local i32)get_local 0f64.const 1f64.eqtee_local 1if i32 get_local 1else get_local 0 f64.const 2 f64.eqendi32.const 1i32.andif f64.const 1 returnendget_local 0f64.const 1f64.subcall 0get_local 0f64.const 2f64.subcall 0f64.addend
这与汇编语言非常相似,其中 f64 是数据类型,f64eq f64.sub f64.Add 是 CPU 指令。
为了将二进制文件格式 wasm 转换为可见的 wast 文本,需要安装 WebAssembly Binary Toolbox WABT,可以通过 Brew Install Wabt 安装在 Mac 上,安装成功后可以使用命令 wasm2wast fWASM 获得 WAST; 除此之外,还可以使用 wast2wasm fwast -o f.wasm 反向转换回来。 除了前面提到的 WebAssembly 二进制工具箱之外,WebAssembly 社区还有以下常用工具:
emscripten:可以将 C、C++* 转换为 WASM、ASMjs;Binaryen:提供更简洁的 IR,将 IR 转换为 WASM,并基于上述 AssemblyScript 提供编译时优化、WASM 虚拟机、WASM 压缩等。 目前,WebAssembly 只能在 JS 中加载和执行,但将来可以在浏览器中加载 JS为了加载和执行 WebAssembly,让我们仔细看看如何在 JS 中调整 WebAssembly。
JS 调优 webassembly 分为 3 大步骤:加载字节码>编译字节码>实例化获取 webassembly 实例后,可以通过 js 调用它,以上 3 个步骤的具体操作分别是:
对于浏览器,您可以使用网络请求来加载字节码,对于 nodejs,您可以通过 fs 模块读取字节码文件。 获取到字节码后,需要将其转换为arraybuffer才能编译,JS apiwebassembly 通过 WebAssembly 传递编译编译时,WebAssembly 将通过 promise 解析module,这不是可以直接调用的要求; 获取模块后,需要使用 WebAssemblyInstanceAPI 实例化模块,获取实例后,您可以像使用 JS 模块一样调用它。 第一步可以在合并步骤中完成,即前面提到的 webassemblyInstantiate 可以同时执行这两项操作。
webassembly.instantiate(bytes).then(mod=>)
前面的例子使用 JS 来调用 WebAssembly 模块,但在某些情况下可能需要在 WebAssembly 模块中调用浏览器 API,下面介绍如何在 WebAssembly 中调用 JS。
webassembly.实例化函数支持第二个参数 webassemblyinstantiate(bytes,importObject),这个importObject参数的目的是将JS传递给WebAssembly,以及WebAssembly中需要调用的JS模块。 举个具体的例子,转换前面的计算斐波那契数列来调用窗口。 在 Web 中的 WebAssembly 中alert 函数会弹出计算结果,为此需要对其进行修改以加载 webassembly 模块
webassembly.instantiate(bytes,})then(mod=>)
对应的还需要修改用 assemblyscript 编写的源代码:
声明命名空间窗口函数 f(x: number): number return f(x - 1) +f(x - 2)}export function f(x: number): void
修改完上面的汇编脚本源代码后,再次使用 asc 和 asc 命令 asc fTS 编译并输出比以前多了几行 wast 文件:
(import "window" "alert" (func $src/asm/module/window.alert (type 0)))func $src/asm/module/f (type 0) (param f64) get_local 0 call $src/asm/module/_f call $src/asm/module/window.alert)
wast ** 的额外部分是调用 AssemblyScript 中 JS 中传递的模块的逻辑。
除了上面常用的 API 之外,WebAssembly 还提供了许多可以通过这个 D 使用的 APITS 文件以查看 WebAssembly JS API 的所有详细信息。 WebAssembly 是一个低级字节码,可以在浏览器和其他环境中运行。
前面提到的 Binaryen 提供了一个工具,可以直接从命令行执行 WASM 二进制文件,通过 Brew Install Binaryen on Mac,然后通过 WASM-shell Fwasm 文件可以直接运行。
目前,V8 JS 引擎增加了对 WebAssembly、Chrome 和 Node 的支持JS 使用 V8 作为引擎,因此 WebAssembly 也可以运行在 Node 上JS环境;
当 V8 JS 引擎运行 WebAssembly 时,WebAssembly 和 JS 在同一个虚拟机中执行,而不是在单独的虚拟机中运行 WebAssembly,这使得 JS 和 WebAssembly 之间更容易相互调用。
在 node 中制作上面的例子js,您可以使用以下命令:
const fs = require('fs');function touint8array(buf) return u;}function loadwebassembly(filename, imports) )loadwebassembly('../temp/assembly/module.wasm') .then(instance => )
在 NodeJS 环境中运行 WebAssembly 并没有多大意义,因为 NodeJS 支持运行本机模块,这些模块的性能比 WebAssembly 更好。 如果你用 C 或 Rust 编写 WebAssembly,你可以直接将其编译成 NodeJS 可以调用的原生模块。
从上面可以看出,WebAssembly 主要是为了解决 JS 的性能瓶颈,即 WebAssembly 适用于需要大量计算的场景,例如:
在浏览器中处理音调 **, flv.使用 WebAssembly 重写后,JS 将得到很大的改进; React 的 DOM Diff 涉及大量的计算,用 WebAssembly 重写 React 核心模块可以提高性能。 Safari 使用的 JS 引擎 J**AscriptCore 也支持 WebAssembly,RN 应用的性能也可以提升。 为了突破大型3D网页游戏的性能瓶颈,白鹭引擎已经开始探索WebAssembly的使用。 尽管 WebAssembly 标准已经最终确定并被主流浏览器实现,但仍然存在以下问题:
浏览器兼容性差,只有最新版本的浏览器支持,不同浏览器对 JS WebAssembly 互调的 API 支持不一致; 生态工具不完善、不成熟,目前还没有编写体验流畅的 WebAssembly 的语言,目前还处于起步阶段; 学习材料太少,需要更多的人去探索和踩坑。 WebAssembly 还不成熟,如果你的团队没有无法忍受的性能问题,那么现在就不是在生产中使用 WebAssembly 的合适时机,因为它会影响团队的生产力,或者遇到不容易解决的陷阱并阻碍开发。