WebAssembly 是一种在Web上运行字节码的编译型语言。
相对于 Javascript,WebAssembly 提供了可预测的性能。 它并不是永远比 JavaScript 快,但是在一些正确的用例中,它的运行效率可以比JavaScript 快。 例如计算密集型任务,例如嵌套循环或处理大量数据。 因此,WebAssembly 是对 JavaScript 的补充,而不是替代品。
WebAssembly 具有极高的可移植性。 WebAssembly 可以运行在:所有主流的 Web浏览器,V8 的运行时环境(如Node.js)和独立的 Wasm 运行时环境(如
Wasmtime
,
Lucet
和
Wasmer
)。
WebAssembly 具有线性内存,换句话说,就是一个很大的可扩展数组。 并且在 Javascript 上下文中,可以通过 Javascript和 Wasm 同步访问。
WebAssembly 可以导出函数和常量,并且可以在Javascript上下文中由 Javascript 和 Wasm 同步访问。
WebAssembly 在其当前的 MVP 中仅处理整数和浮点数。 但是,存在工具和库可以使高级数据类型的传递变得方便。
什么是 AssemblyScript ?
AssemblyScript 本质上是 WebAssembly 编译器的 TypeScript。可以说是专门为想要编写 WebAssembly 而又不想学习新语言的前端开发人员准备的。 但是,由于 WebAssembly 与 TypeScript / JavaScript 的区别,不能直接将我们的 TypeScript 应用程序(比如 React + TypeScript 的技术栈)编译为 WebAssembly,这是语言本身上的区别和限制造成的,一些 JavaScript 的特性和 Typescript 的类型我们是不能够使用的。 但是如果从零开始搭建 WebAssembly 应用的话 AssemblyScript 仍然是一个好的选择。
浏览器中的 WebAssembly
WebAssembly
JavaScript 对象是所有
WebAssembly
相关功能的命名空间。
和大多数全局对象不一样,
WebAssembly
不是一个构造函数(它不是一个函数对象)。它类似于
Math
对象或者
Intl
对象,Math 对象也是一个命名空间对象,用于保存数学常量和函数,Intl 则是用于国际化和其他语言相关函数的命名空间对象。
为了理解 WebAssembly 如何在浏览器中运行,需要了解几个概念。
下面的概念都被抽象为了一个对象
模块(Module)
包含已经由浏览器编译的无状态 WebAssembly 代码,可以高效地与 Workers 共享(通过
postMessage()
函数)、缓存在 IndexedDB 中,和多次实例化。一个模块可以被当做一个 JavaScript 模块导入导出。
创建方法:
WebAssembly.Module()
构造函数可以用来同步编译给定的 WebAssembly 二进制代码。
通过异步编译函数,如
WebAssembly.compile()
方法,或者是通过 IndexedDB 读取 Module 对象。
注:
由于大型模块的编译可能很消耗资源,开发人员只有在绝对需要同步编译时,才使用
Module()
构造函数;其他情况下,应该使用异步
WebAssembly.compile()
方法。
内存(Memory)
一个可调整大小的
ArrayBuffer
,其内存储的是 WebAssembly
实例
所访问内存的原始字节码。从 JavaScript 或 WebAssembly 中所创建的内存,可以由 JavaScript 或 WebAssembly 来访问及更改。
创建方法:
WebAssembly.Memory()
构造函数创建一个新的
Memory
对象。该对象的
buffer
属性即可获取到需要的
ArrayBuffer
表格(Table)
具有类数组结构,存储了多个函数引用。在 Javascript 或者 WebAssemble 中创建 Table 对象可以同时被 Javascript 或 WebAssemble 访问和更改。
创建方法:
一般会通过
WebAssembly.Table()
构造函数根据给定的大小和元素类型创建一个 Table 对象。
实例(Instance)
WebAssembly.Module
的一个可执行实例,本身就具有状态,实例中有一个 exports 对象包含了所有 WebAssembly 的导出,可以使用 JavaScript 调用 WebAssembly 代码。
创建方法:
使用
WebAssembly.Instance()
构造函数以同步方式实例化一
WebAssembly.Module
对象。
通过异步函数
WebAssembly.instantiate()
。
注:
由于大型模块的实例化代价极高, 开发人员应只在必须同步实例化的时候,才使用
Instance()
,绝大多数情况应该使用异步方法
WebAssembly.instantiate()
。
全局(Global)
在前面我们知道了 WebAssembly 的一个特性模块性,但 WebAssembly 同样允许我们跨越多个模块共享内存信息,跨越一个或多个
WebAssembly.Module
实例,允许被多个modules动态连接。
创建方法:
使用
WebAssembly.Global
对象表示一个全局变量实例,该对象可以被 JavaScript 和
importable/exportable
访问。
下面的方法除了
WebAssembly.validate()
都是返回的
Promise
,主要看一下
WebAssembly.instantiate()
和
WebAssembly.instantiateStreaming()
两个方法,一般都是使用这两个方法加载 wbsm 模块
WebAssembly.compile()
WebAssembly.compile()
方法编译 WebAssembly 二进制代码到一个
WebAssembly.Module
对象。如果在实例化之前有必要去编译一个模块,那么这个方法是有用的(否则,将会使用
WebAssembly.instantiate()
方法)。
WebAssembly.compileStreaming()
WebAssembly.compileStreaming()
方法用来从一个流式源中直接编译一个
WebAssembly.Module
。当模块需要在被实例化前被编译时,这个方法会很有用。如果要从流式源实例化一个模块应采用
WebAssembly.instantiateStreaming()
方法。
WebAssembly.validate()
WebAssembly.validate()
方法用于验证包含 WebAssembly 二进制码的一个
typed array
是否合法,如果这些字节能构成一个合法的 wasm 模块则返回
true
,否则返回
false
。
WebAssembly.instantiate()
用于编译和实例化 WebAssembly 代码的主 API,返回一个
Module
和它的第一个
Instance
实例。这个方法有两个重载方式:
第一种主要重载方式使用 WebAssembly 二进制代码的
typed array
或
ArrayBuffer
,一并进行编译和实例化。返回的
Promise
会携带已编译的
WebAssembly.Module
和它的第一个实例化对象
WebAssembly.Instance
。
第二种重载使用已编译的
WebAssembly.Module
, 返回的
Promise
携带一个
Module
的实例化对象
Instance
. 如果这个
Module
已经被编译了或者是从缓存中获取的, 那么这种重载方式是非常有用的。
注:
此方法不是获取(fetch)和实例化wasm模块的最具效率方法。 如果可能的话,应该改用较新的
WebAssembly.instantiateStreaming()
方法,该方法直接从原始字节码中直接获取,编译和实例化模块,因此不需要转换为
ArrayBuffer
。
WebAssembly.instantiateStreaming()
直接从流式底层源编译和实例化 WebAssembly 模块,同时返回
Module
及其第一个
Instance
实例。
下面是对上面两个方法做的封装:
export const wasmBrowserInstantiate = async (
wasmModuleUrl,
importObject
) => {
let response
if (typeof importObject?.env ?.abort !== 'function' ) {
importObject = Object .assign ({}, importObject, {
env : {
abort : () => console .log ('Abort!' )
if (WebAssembly .instantiateStreaming ) {
response = await WebAssembly .instantiateStreaming (
fetch (wasmModuleUrl),
importObject
} else {
const fetchAndInstantiateTask = async () => {
const wasmArrayBuffer = await fetch (wasmModuleUrl).then ((response ) =>
response.arrayBuffer ()
return WebAssembly .instantiate (wasmArrayBuffer, importObject)
response = await fetchAndInstantiateTask ()
return response
如何使用:
import { wasmBrowserInstantiate } from './wbsm-util'
const wasmModule = await wasmBrowserInstantiate ('www.xxx.com/index.wasm' )
至于怎么具体使用wasm
模块,下面会有详细介绍。
AssemblyScript 的使用
构建一个 AssemblyScript 的应用程序
如果你不想要在本地构建项目,可以试试使用 WebAssembly Studio 在线构建
对于前端开发者而言十分简单,使用npm
就能下载相关依赖项:
首先创建一个空项目,然后下载下面两个依赖项:
npm install --save @assemblyscript/loader
npm install --save-dev assemblyscript
npx asinit .
它会自动在我们项目中创建初始的推荐目录结构和配置文件用于开发:
目录结构如下:
asconfig
是asc
命令的配置文件,用于生成对应的.wasm
文件
assembly
目录就是我们实际的开发目录
build
目录为我们将 TypeScript 转换为 WebAssembly 之后的默认构建目录
index.js
文件用于引入我们编译后的.wasm
文件然后使用相应的loader
解析并给其余文件使用:
const fs = require ("fs" );
const loader = require ("@assemblyscript/loader" );
const imports = { };
const wasmModule = loader.instantiateSync (fs.readFileSync (__dirname + "/build/optimized.wasm" ), imports);
module .exports = wasmModule.exports ;
tests
目录即测试目录,下面的文件会引入上面的index.js
,用于测试编译后的模块,但是这种测试方式过于繁琐,社区内已经专门为 AssemblyScript 开发了对应基于jest
的单元测试库,后续会进行说明,所以这个目录可以删掉。
在浏览器中引入 wasm 模块
现在打开assembly/index.ts
,可以看到下面的代码:
export function add (a: i32, b: i32 ): i32 {
return a + b;
我们会看到一个不同于之前写TypeScript
的类型i32
,但实际上它的定义是这样的:
declare type i32 = number ;
可以看到,他本身只是一个number
的类型别名,在WebAseembly
中代表的是一个 32 位的有符号整型,除此之外还有i64
、f32
、f64
等这样单独代表 64 位整型和浮点型的类型,可以看出 webAseembly 同 Java 等一样将整形与浮点型的数值分开进行了声明。因为需要符合 WebAssembly 的相关特性,所以我们在类型的指定也不能同普通的 TypeScript 语法一样了。
OK,我们先把它复制到assembly/Hello-World/index.ts
文件夹下,再把它编译一下(先使用命令编译):
npx asc assembly/Hello-World/index.ts -b build/Hello-World/index.wasm
可以看到,在build
目录下面生成了对应的hello-world
目录和编译后的wasm
文件,我们来把它们引入到浏览器中:
import { instantiate } from '@assemblyscript/loader'
const runWasmAdd = async () => {
const wasmModule = await instantiate (fetch ('../build/Hello-World/index.wasm' ))
const addResult = wasmModule.exports .add (24 , 24 )
document .body .textContent = `Hello World! addResult: ${addResult} `
runWasmAdd ()
<!DOCTYPE html >
<meta charset ="UTF-8" />
<title > Hello World - AssemblyScript</title >
<script type ="module" src ="./01.hello-world.js" > </script >
</head >
<body > </body >
</html >
wasm 模块的导出
export function callMeFromJavascript (a: i32, b: i32 ): i32 {
return addIntegerWithConstant (a, b)
export const GET_THIS_CONSTANT_FROM_JAVASCRIPT : i32 = 2424
function addIntegerWithConstant (a: i32, b: i32 ): i32 {
return a + b + ADD_CONSTANT
const ADD_CONSTANT : i32 = 1
npx asc assembly/Exports/index.ts -b build/Exports/index.wasm
import { instantiate } from '@assemblyscript/loader'
const runWasm = async () => {
const wasmModule = await instantiate (fetch ('../build/Exports/index.wasm' ))
const exports = wasmModule.instance .exports
console .log (exports .callMeFromJavascript (24 , 24 ))
console .log (exports .GET_THIS_CONSTANT_FROM_JAVASCRIPT )
console .log (exports .GET_THIS_CONSTANT_FROM_JAVASCRIPT .valueOf ())
console .log (exports .GET_THIS_CONSTANT_FROM_JAVASCRIPT .value )
console .log (exports .addIntegerWithConstant )
runWasm ()
我们可以在控制台看到打印效果:
与 JavaScript 共享内存
线性内存是无符号字节的连续缓冲区,可以由 Wasm 和 JavaScript 读取和存储。 换句话说,Wasm 内存是 JavaScript 和 Wasm 可以同步读取和修改的字节的可扩展数组。 线性内存可以用于很多事情,其中之一是在 Wasm 和 JavaScript 之间来回传递值。
memory.grow (1 )
const index = 0
const value = 24
store<u8>(index, value)
export function readWasmMemoryAndReturnIndexOne (): i32 {
let valueAtIndexOne = load<u8>(1 )
return valueAtIndexOne
npx asc assembly/WebAssembly-Linear-Memory/index.ts -b build/WebAssembly-Linear-Memory/index.wasm
import { instantiate } from '@assemblyscript/loader'
const runWasm = async () => {
const wasmModule = await instantiate (
fetch ('../build/WebAssembly-Linear-Memory/index.wasm' )
const exports = wasmModule.instance .exports
const memory = exports .memory
const wasmByteMemoryArray = new Uint8Array (memory.buffer )
console .log (wasmByteMemoryArray[0 ])
wasmByteMemoryArray[1 ] = 25
console .log (exports .readWasmMemoryAndReturnIndexOne ())
runWasm ()
在 WebAssembly 中导入 JavaScript 的函数
这里我们说一下在上面构建一个 AssemblyScript 的应用程序
中初始摸版提到的imports
对象,该对象可以让我们在 Wasm 模块中注入 JavaScript 的变量或函数并在其中调用。
注意: 编译的模块中,对于每一个导入的值都要有一个与其匹配的属性与之相对应,否则会抛出错误。
declare function consoleLog (arg0: i32 ): void
consoleLog (24 )
npx asc assembly/Importing-Javascript-Functions-Into-WebAssembly/index.ts -b build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm
import { instantiate } from '@assemblyscript/loader'
const runWasm = async () => {
const wasmModule = await instantiate (
fetch (
'../build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
index : {
consoleLog : (value ) => console .log (value)
runWasm ()
自定义导入名称
我们可以使用@external
装饰器修改我们要从外界导入进来的函数名称,@external
装饰器可以传入一个或两个参数,如果传入一个参数则代表修改函数或变量名,如果传入两个参数则代表修改模块名和函数或变量名。
@external ("consoleLog3" )
declare function consoleLog (arg0: i32 ): void
@external ("foo" , "consoleLog4" )
declare function consoleLog2 (arg0: i32 ): void
consoleLog (24 )
consoleLog2 (25 )
可以看出,即使是在同一个文件中,AssemblyScript 在编译时也可以将其单独分配在不同的模块中使用。
import { instantiate } from '@assemblyscript/loader'
const runWasm = async () => {
const wasmModule = await instantiate (
fetch (
'../build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
index : {
consoleLog3 : (value ) => console .log (value)
foo : {
consoleLog4 : (value ) => console .log (value)
runWasm ()
WebAssembly 非常适合计算密集型任务,甚至官方的 AssemblyScript 文档都涵盖了这一点。例如,涉及大数据,带有条件的繁重逻辑或嵌套循环之类的任务。 因此,通过将上述任务移至 WebAssembly 中,可以生成/渲染图形,从而显着提高速度。
在下面的示例中,我们将每秒生成 20x20 个彩色棋盘图像,并使用ImageData
对象上的Pixel Manipulation将它们显示在canvas
上。 用图形术语来说,这是一个光栅图。
memory.grow (1 )
const CHECKERBOARD_SIZE : i32 = 20
export const CHECKERBOARD_BUFFER_POINTER : i32 = 0
export const CHECKERBOARD_BUFFER_SIZE : i32 =
CHECKERBOARD_SIZE * CHECKERBOARD_SIZE * 4
export function generateCheckerBoard (
darkValueRed: i32,
darkValueGreen: i32,
darkValueBlue: i32,
lightValueRed: i32,
lightValueGreen: i32,
lightValueBlue: i32
): void {
for (let x : i32 = 0 ; x < CHECKERBOARD_SIZE ; x++) {
for (let y : i32 = 0 ; y < CHECKERBOARD_SIZE ; y++) {
let isDarkSquare : boolean = true
if (y % 2 === 0 ) {
isDarkSquare = false
if (x % 2 === 0 ) {
isDarkSquare = !isDarkSquare
let squareValueRed = darkValueRed
let squareValueGreen = darkValueGreen
let squareValueBlue = darkValueBlue
if (!isDarkSquare) {
squareValueRed = lightValueRed
squareValueGreen = lightValueGreen
squareValueBlue = lightValueBlue
let squareNumber = y * CHECKERBOARD_SIZE + x
let squareRgbaIndex = squareNumber * 4
store<u8>(
CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 0 ,
squareValueRed
)
store<u8>(
CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 1 ,
squareValueGreen
)
store<u8>(
CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 2 ,
squareValueBlue
)
store<u8>(CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 3 , 255 )
npx asc assembly/Reading-and-Writing-Graphics/index.ts -b build/Reading-and-Writing-Graphics/index.wasm
import { instantiate } from '@assemblyscript/loader'
const runWasm = async () => {
const wasmModule = await instantiate (
fetch ('../build/Reading-and-Writing-Graphics/index.wasm' )
const exports = wasmModule.instance .exports
const memory = exports .memory
const wasmByteMemoryArray = new Uint8Array (memory.buffer )
const canvasElement = document .querySelector ('canvas' )
const canvasContext = canvasElement.getContext ('2d' )
const canvasImageData = canvasContext.createImageData (
canvasElement.width ,
canvasElement.height
const getDarkValue = () => {
return Math .floor (Math .random () * 100 )
const getLightValue = () => {
return Math .floor (Math .random () * 127 ) + 127
const drawCheckerBoard = () => {
exports .generateCheckerBoard (
getDarkValue (),
getDarkValue (),
getDarkValue (),
getLightValue (),
getLightValue (),
getLightValue ()
const imageDataArray = wasmByteMemoryArray.slice (
exports .CHECKERBOARD_BUFFER_POINTER .value ,
exports .CHECKERBOARD_BUFFER_SIZE .value
canvasImageData.data .set (imageDataArray)
canvasContext.clearRect (0 , 0 , canvasElement.width , canvasElement.height )
canvasContext.putImageData (canvasImageData, 0 , 0 )
drawCheckerBoard ()
setInterval (() => {
drawCheckerBoard ()
}, 1000 )
runWasm ()
<!DOCTYPE html >
<meta charset ="UTF-8" />
<title > Reading-and-Writing-Graphics</title >
<script type ="module" src ="./05-reading-and-writing-graphics.js" > </script >
</head >
<canvas
width ="20"
height ="20"
style ="
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100%;
</canvas >
</body >
</html >
这个例子只是为了演示如何在 AssemblyScript 中读写音频,但是生产环境其实并不会这样做,我们这样写也只是出于学习的目的。
该示例会发出噪声,注意音量!!!
同样的,可以在 WebAssembly 中生成/渲染音频样本,从而显着提高速度。
在下面的示例中,我们将使用Web Audio API
放大AudioBuffer
中的音频样本。
注意: 可以并且应该通过GainNode
来完成此功能,但这主要是为了演示的目的。或者我们可以想象为实现不受支持的Web Audio API
效果,例如bitcrusher
或针对不受支持的浏览器的ogg
解码器。
memory.grow (1 )
export const INPUT_BUFFER_POINTER : i32 = 0
export const INPUT_BUFFER_SIZE : i32 = 1024
export const OUTPUT_BUFFER_POINTER : i32 =
INPUT_BUFFER_POINTER + INPUT_BUFFER_SIZE
export const OUTPUT_BUFFER_SIZE : i32 = INPUT_BUFFER_SIZE
export function amplifyAudioInBuffer (): void {
for (let i = 0 ; i < INPUT_BUFFER_SIZE ; i++) {
let audioSample : u8 = load<u8>(INPUT_BUFFER_POINTER + i)
if (audioSample > 127 ) {
let audioSampleDiff = audioSample - 127
audioSample = audioSample + audioSampleDiff
} else if (audioSample < 127 ) {
audioSample = audioSample / 2
store<u8>(OUTPUT_BUFFER_POINTER + i, audioSample)
npx asc assembly/Reading-and-Writing-Audio/index.ts -b build/Reading-and-Writing-Audio/index.wasm
import { instantiate } from '@assemblyscript/loader'
const audioContext = new (window .AudioContext || window .webkitAudioContext )()
const numberOfSamples = 1024
const audioBuffer = audioContext.createBuffer (
numberOfSamples,
audioContext.sampleRate
const originalAudioSamples = new Float32Array (numberOfSamples)
const amplifiedAudioSamples = new Float32Array (numberOfSamples)
const floatSamplesToByteSamples = (floatSamples ) => {
const byteSamples = new Uint8Array (floatSamples.length )
for (let i = 0 ; i < floatSamples.length ; i++) {
const diff = floatSamples[i] * 127
byteSamples[i] = 127 + diff
return byteSamples
const byteSamplesToFloatSamples = (byteSamples ) => {
const floatSamples = new Float32Array (byteSamples.length )
for (let i = 0 ; i < byteSamples.length ; i++) {
const byteSample = byteSamples[i]
const floatSample = (byteSample - 127 ) / 127
floatSamples[i] = floatSample
return floatSamples
const runWasm = async () => {
const wasmModule = await instantiate (
fetch ('../build/Reading-and-Writing-Audio/index.wasm' )
const exports = wasmModule.instance .exports
const memory = exports .memory
const wasmByteMemoryArray = new Uint8Array (memory.buffer )
const sampleValue = 0.3
for (let i = 0 ; i < numberOfSamples; i++) {
if (i < numberOfSamples / 2 ) {
originalAudioSamples[i] = sampleValue
} else {
originalAudioSamples[i] = sampleValue * -1
const originalByteAudioSamples = floatSamplesToByteSamples (
originalAudioSamples
wasmByteMemoryArray.set (
originalByteAudioSamples,
exports .INPUT_BUFFER_POINTER .value
exports .amplifyAudioInBuffer ()
const outputBuffer = wasmByteMemoryArray.slice (
exports .OUTPUT_BUFFER_POINTER .value ,
exports .OUTPUT_BUFFER_POINTER .value + exports .OUTPUT_BUFFER_SIZE .value
const outputFloatAudioSamples = byteSamplesToFloatSamples (outputBuffer)
amplifiedAudioSamples.set (outputFloatAudioSamples)
runWasm ()
function beforePlay () {
if (audioContext.state === 'suspended' ) {
audioContext.resume ()
let audioBufferSource
function stopAudioBufferSource () {
if (audioBufferSource) {
audioBufferSource.stop ()
audioBufferSource = undefined
function createAndStartAudioBufferSource () {
stopAudioBufferSource ()
audioBufferSource = audioContext.createBufferSource ()
audioBufferSource.buffer = audioBuffer
audioBufferSource.loop = true
audioBufferSource.connect (audioContext.destination )
audioBufferSource.start ()
const playBtn = document .getElementById ('play-btn' )
const amplifiedBtn = document .getElementById ('amplified-btn' )
const pauseBtn = document .getElementById ('pause-btn' )
playBtn.addEventListener ('click' , playAmplified)
amplifiedBtn.addEventListener ('click' , playOriginal)
pauseBtn.addEventListener ('click' , pause)
function playOriginal () {
beforePlay ()
audioBuffer.getChannelData (0 ).set (originalAudioSamples)
audioBuffer.getChannelData (1 ).set (originalAudioSamples)
createAndStartAudioBufferSource ()
function playAmplified () {
beforePlay ()
audioBuffer.getChannelData (0 ).set (amplifiedAudioSamples)
audioBuffer.getChannelData (1 ).set (amplifiedAudioSamples)
createAndStartAudioBufferSource ()
function pause () {
beforePlay ()
stopAudioBufferSource ()
<!DOCTYPE html >
<meta charset ="UTF-8" />
<title > Reading-and-Writing-Audio</title >
<script type ="module" src ="./06-reading-and-writing-audio.js" > </script >
</head >
<h1 > NOTE: Be careful if using headphones</h1 >
<h1 > Original Sine Wave</h1 >
<div > <button id ="play-btn" > Play</button > </div >
<h1 > Amplified Sine Wave</h1 >
<button id ="amplified-btn" > Play</button >
</div >
<h1 > Pause</h1 >
<div > <button id ="pause-btn" > Pause</button > </div >
</body >
</html >
与 TypeScript 的不同
因为之前并没有太多用到下面这些特性,所以我决定先从例子入手了解一下 AssemblyScript 的使用,再说一下和 TypeScript 的不同点。
之前我们有提到过,AssemblyScript 是编译为 WebAssembly 的,而 TypeScript 是编译为 JavaScript,在这两点上就有着本质区别,AssemblyScript 会更像 C、C++ 或 Rust 这些语言,所以 WebAssembly 有着更加严格的静态类型约束。
更加约束的静态类型
编写 AssemblyScript 时首先要注意的一点是,它的基本类型与 TypeScript 的基本类型有所不同,因为它使用 WebAssembly 的更特定的整数和浮点类型,而 JavaScript 的数字只是 WebAssembly 的f64
的别名。
JavaScript JIT 会尝试在代码执行时找出数字值的最佳表示形式,并假设数值会改变类型,并可能多次重新编译代码,而 AssemblyScript 则允许开发人员提前及其所有优点和优点指定理想的类型。
没有 any 和 undefined
function foo (a? ) {
const b = a + 1
return b
function foo (a: i32 = 0 ): i32 {
const b = a + 1
return b
没有联合类型
function foo (a: i32 | string ): void {
function foo<T>(a : T): void {
严格类型的对象
相比起 TypeScript,AssemblyScript 更加的偏向静态 ,在对于对象创建来说,我们不能直接使用对象字面量声明,而是需要使用new Map()
这样的形式创建对象。
const obj1 = {};
obj.a = 1 ;
const obj2 = new Map ();
obj2.set ('a' , 1 );
不同的全等于(===)
===
运算符的特殊语义(当值和类型都匹配时才为true
)在 AssemblyScript 中没有多大意义,因为比较两个不兼容类型的值无论如何都是非法的。 因此,===
运算符已被重新用于执行身份比较,如果两个操作数都是相同的对象,则评估为true
:
const a = "hello"
const b = a
const c = "h" + a.substring (1 )
if (a === b) { }
if (a === c) { }
if (a == c) { }
对于 Null 值的检查
像 TypeScript 一样,AssemblyScript 编译器可以检查传递的值是否可为空:
function doSomething (something: string | null ): void {
if (something) {
something.length
但是,这不适用于某个对象的属性,因为它们的值可以在检查和使用值之间改变:
class Foo {
something : string | null = null ;
function doSomething (foo: Foo ): void {
if (foo.something ) {
foo.something .length
如果我们在运行时将foo.something
设置为了null
,再次访问时 TypeScript 会报运行时错误。
class Foo {
something : string | null = null ;
function doSomething (foo: Foo ) {
if (foo.something ) {
doSomethingElse (foo);
foo.something .length ;
function doSomethingElse (foo: Foo ) {
foo.something = null ;
但是,AssemblyScript 中不会出现这样的运行时错误,因此,为了安全起见,必须使用本地变量:
function doSomething (foo: Foo ): void {
let something = foo.something
if (something) {
something.length
对于单元测试,我们使用社区提供的 as-pect 。
集成在项目中也很简单,我们只需要在之前创建好的项目中安装as-pect
即可
npm install --save-dev @as-pect/cli
然后运行下面的命令:
npx asp --init
脚手架会自动在我们的项目中创建对应的初始化文件:
在package.json
中添加脚本:
"scripts" : {
"test" : "asp --verbose" ,
"test:ci" : "asp --summary"
默认使用根目录下的as-pect.config.js
文件作为配置文件,也可以使用--config
主动指定配置文件。具体配置和使用可以查看其官方文档,这里就不多赘述了。
做一个总结,本文先从浏览器中的 WebAssembly Api 开始介绍,讲了其相关特性和方法,大致讲述了如何在浏览器中引入 WebAssembly 模块。然后使用了几个例子介绍了如何在开发中集成与使用 AssemScript ,并比较了其与传统 TypeScript 的不同点,因为编译后的产物不同,我们需要有不一样的注意点。最后再简单地介绍了一下 AssemblyScript 的单元测试工具的使用。
总的来说,AssemblyScript 很大程度上帮助前端开发人员深入接触到了 WebAssembly 的世界,我们可以在进行大量运算的时候转入 AssemblyScript 来生成wasm
模块,并且能够很轻松地投入到生产中。
最后,作者技术有限,如果有什么错误或遗漏的地方还请帮忙指出,我会在第一时间修改。
本文的所有代码已上传到 github
The AssemblyScript Book
Wasm By Example
as-pect
MDN - WebAssembly
初识 WebAssembly
1609
WebAssembly
JavaScript
TypeScript
3006
JavaScript
WebAssembly
3541
腾讯IVWEB团队
WebAssembly
3047
Gavin1995
WebAssembly
1525
xlaoyu
WebAssembly
JavaScript
1233
WebAssembly
JavaScript
1963
专有钉钉前端团队
WebAssembly