JavaScript 填坑之 flow 和 WebAssembly 技术

js的坑

作为一门只用了十天时间赶工出来的动态语言, JavaScript已经做得很好了, 但是有些不得不解决的问题也逐渐暴露出来了. 下面是一些坑列表.

  • 运行性能, 由googlev8引擎解决.
  • 类型继承, 由ES6语法糖和phototype对象构成原型链解决.
  • 模块化, 由ES6importexports实现.
  • 全局变量(window), 社区方案使用Global统一替换.
  • 回调函数机制, 由promise解决.
  • 动态语言导致的大型项目类型检查, 使用React.PropTypesflowjs, 或者使用TypeScript.
  • v8引擎编译性能, WebAssembly技术.

flowjs

flowjsFacebook为了大型js项目拥有类型检查而发明的一个库. 使那些需要类型检查的项目不需要使用重量级的TypeScript的另一个解决方案. 基本作用是通过可选的类型声明, 在写代码时自动检查类型是否匹配. 语法也不会很麻烦, 基本都是在变量后面加上:type来声明. 但是我看了一圈, 好像没有解构赋值时的类型语法, 这样就有点麻烦了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* @flow */

function foo(x: ?number): string {
if (x) {
return x;
}
return "default string";
}

//报错:
5: return x;
^ number. This type is incompatible with the expected return type of
3: function foo(x: ?number): string {
^ string

v8引擎

在说WebAssembly之前, 必须看看v8引擎的原理和不足. chrome现在这么高的市场占有率有一半都是靠着v8引擎的速度取得的.

v8引擎底层由C++实现, 可以作为一个独立模块, 被任何C++应用引用. 如Node.js框架就是基于v8引擎. v8基本功能也是编译, 执行js代码, 管理内存, 负责垃圾回收, 与宿主语言交互等功能. 而这种运行时编译的语言都存在一个问题, 即类型改变. v8中编译技术为JIT(Just in time compiling), JIT基于运行期分析编译, 而JavaScript是一个没有类型的语言, 于是大部分时间, JIT编译器其实是在猜测JavaScript中的类型, 如开始一个函数执行的是add(1,1), 那么JIT会将它编译成add(int a,int b)来执行, 并保存, 但如果下次调用add('a','b'), 那么JIT则需要重新编译一遍. 在js这种动态类型的情况下, 一个函数会发生很多次编译, 重新编译开销又非常大, 这便是v8或者说js引擎无法避免的问题. Google也发明过声明, 认为js是一门天生残疾的语言, 然后就不再继续发力v8虚拟机了. v8其实已经事实上代表了js能达到的运行时性能顶峰, 而Google也认为基于这种动态类型无法再继续提升虚拟机性能了.

asm.js

asmmozilla提出的一个解决方案, 和js语法相同, 只是在火狐浏览器中会特别解析语法, 直接编译成机器语言, 极大的加快执行速度. 但是存在的一个问题是浏览器支持率并不高, 而且也不是官方解决方案.

WebAssembly

这时候, 几大浏览器厂家(Mozilla, Google, Microsoft, Apple)觉得这种思路还是可以的, 那么就升级一下, 变为WebAssembly技术. 这种技术基本等于浏览器执行原生代码. 和Java调用JNI一样的速度.

start

需要安装的内容有点多, 参考官方文档所需工具.

在非开发版的浏览器中, FirefoxWebAssembly支持比较好, 在浏览器控制台中粘贴以下代码, 如果可以运行而且不报错, 说明支持WebAssembly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WebAssembly.compile(new Uint8Array(`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)).then(module => {
const instance = new WebAssembly.Instance(module)
const { add, square } = instance.exports

console.log('2 + 4 =', add(2, 4))
console.log('3^2 =', square(3))
console.log('(2 + 5)^2 =', square(add(2 + 5)))
})

//运行结果
Promise { <state>: "pending" }
2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

WebAssembly有明确的数据类型, 上面函数运行时传入参数应该为int 32类型, 不过如果传其他值会被动态转换, 所以add('2',3) = 5, add('2',{a:1}) = 2. 但是不推荐这样使用, 因为具体会转换成什么不能确定, 通常是0.

编译C/C++WebAssembly

使用WebAssembly技术, 肯定不能直接写二进制代码, 否则为什么要用js. WebAssembly一般都是由其他语言编译过来, 如C/C++等. 网上有个在线工具可以查看WasmExplorer, 生成的wast文件可以转换成wasm文件并下载下来.

先写一段C代码, 然后执行emcc demo.c -Os -s WASM=1 -s SIDE_MODULE=1 -o demo.wasm, 即可生成wasm文件.

执行wasm文件

执行wasm目前只有一种方法, 即通过javascript. 官网文档里也有提供初略的例子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//demo.c
int addThree(int a, int b, int c){
return a + b + c;
}

//index.html
function loadWebAssembly(filename, imports = {}) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
imports.env = imports.env || {}
Object.assign(imports.env, {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
})
return new WebAssembly.Instance(module, imports)
})
}

loadWebAssembly('/demo.wasm')
.then(instance => {
const addThree = instance.exports._addThree
console.log('1 + 2 + 3 =', addThree(1, 2, 3))
})

asm.js编译成WebAssembly

asm.js既然是带类型的js, 那么也可以生成WebAssembly文件. 官方有提供工具BinaryenWABT (WebAssembly Binary Toolkit). 流程基本是由js生成wast文件, 再转换成wasm文件.

WebAssembly调用JavaScript

目前来说, 调用方法还是比较麻烦的, 需要将调用的函数通过imports参数传入模块中, 而且需要在wast文件中, 添加import和调用call方法才可以执行. 期待以后简单点的写法互相调用, 那么就可以实现任何语言来写js代码了, 而且运行速度将大大加快.

JavaScript 填坑之 flow 和 WebAssembly 技术

https://mosby-zhou.github.io/2017/11-16-flowjs-and-webassembly/

作者

Mosby

发布于

2017-11-16

许可协议

评论