这几天前端圈最火的事件莫过于 ry(Ryan Dahl) 的新项目 deno 了,不少 IT 新闻和媒体都用了标题:“下一代 Node.js”。这周末读了一遍 deno 的源码,特地写了这篇文章。长文预警(5000字,11图)。前端
这是我上周作的一张图,介绍了 JavaScript 的发展简史。刚才修改了一下,添加了对 Node.js 和 Deno 发布时间的标注。 Node.js 和 Deno 分别是 Ryan Dahl 在 2009 年和 2018 年,基于当年最新的前端技术开发的非浏览器 JavaScript 运行时。java
Ryan Dahl 开发 deno 并非由于 “just for fun”,也不是为了取代 node。下面慢慢解释。node
这两天花时间看了 deno 的源码(好在是初级阶段,源码不多,也很容易理解),顺带看了全部的 issue 和 pr。不知道“从官方介绍来看,能够认为它是下一代 Node”是如何脑补出来的。git
既然是 Node.js 之父的新做,在讨论中天然离不开 Node.js。而做者很皮的回复到:程序员
The main difference is that Node works and Deno does not work : )github
最大的区别就是:Node 能够工做,而 Deno 不行 : )web
目前 Deno 只是一个 Demo,甚至连二进制发行版都没有。好在从源码编译比较简单(若是你使用的不是 Windows 系统)。typescript
在 high-level 层面,Deno 提供了一个尽量简单的 V8 到系统 API 的绑定。为何使用 Golang 替代 C++ 呢,由于相比 Node 而言,Golang 让咱们更加容易的添加新特性,好比 http2 等。npm
至于为何不选择 Rust,做者没有回答。后端
咱们再对比一下二者的启动性能。分别运行:
console.log('Hello world')
复制代码
我以前写过一篇文章:Node.js 新计划:使用 V8 snapshot 将启动速度提高 8 倍,那若是咱们使用 --without-snapshot
参数编译 Node.js 呢?
依然是相差悬殊,毕竟 deno 须要加载一个 TypeScript 编译器。毕竟是一个 demo 版本,但愿之后用力优化。
对于性能提高还有一个思路就是,可使用 LLVM 做为后端编译器把 TypeScript 代码编译为 WebAssembly 而后在 V8 里面运行,甚至能够直接把源码编译成二进制代码运行。Ryan Dahl 表示 deno 只须要一个编译器,那就是 TS。可是既然 deno 要兼容浏览器,那么 WebAssembly 应该也会被支持。
Deno 能够对 ts 的编译结果进行缓存(~/.deno/cache
),因此目前关注的就是启动速度和初次编译速度。
要么就是在发布前先行编译,如此一来 deno 就脱离了开发的初衷了。deno 是一个 ts 的运行时,那么就应该能够直接运行 ts 代码,若是提早把 ts 编译成 js,那么 deno 就回退到 js 运行时了。
对于这个问题,Ryan Dahl 的回答干净利落:
Use Node. Deno is a prototype / experiment.
使用 Node。Deno 只是一个原型或实验性产品。
从介绍能够看到,Deno 的目标是不兼容 Node,而是兼容浏览器。
因此,Deno 不是要取代 Node.js,也不是下一代 Node.js,也不是要放弃 npm 重建 Node 生态。deno 的目前是要拥抱浏览器生态。
不得不说这个目标真伟大。Ryan Dahl 开发了 Node.js,社区构建出了整个 npm 生态。我在另外一个回答 justjavac:纯前端开发眼里nodejs究竟是什么? 里面写到“Node.js 是前端工程化的重要支柱之一”。
虽而后来 Ryan Dahl 离开 Node.js 去了 Golang 社区,可是如今 Ryan Dahl 又回来了,为 JavaScript 社区带来了 Golang,开发出了 Deno,而后拥抱浏览器生态。👍
咱们看看 deno 的关于 Web API 的目标:
甚至还会包括 webGL 和 GPU 等的支持。
Parsa Ghadimi 绘制了一张关于 Deno 的架构图:
底层使用了做者开发的 v8worker2,而 event-loop 则基于 pub/sub 模型。关于 v8worker 能够看看这个 PPT:docs.google.com/presentatio…
我比较好奇的是 deno 使用了 protobuf,而没有使用 Mojo。既然目标是要兼容浏览器,却不使用 Mojo,而是要在 protobuf 上从新造轮子,可见 Ryan Dahl 是真正的“轮子哥”啊。若是想要兼容浏览器生态,选择 Mojo 是个捷径,而若是目标是高性能的服务器,那么应该选择非序列化的 zero-copy 库。不管从哪一个角度看 protobuf 好像都不太适合 deno。可是从 issue 中能够看出,Ryan Dahl 以前是没有据说过 Mojo 的,可是他看完 mojo 以后,依然以为 protobuf 的选择是正确的。
Mojo 是 Google 开发的新一代 IPC 机制,用以替换旧的 Chrome IPC。目前 Chrome 的最新版本是 67,而 Google 的计划是在 2019 年的 75 版本用 mojo 替换掉全部的旧的 IPC。
Mojo 的思路确实和 protobuf 毕竟像,毕竟都是 Google 家的。旧的 IPC 系统是基于在 2 个进程(线程)之间的命名管道(IPC::Channel)实现的。这个管道是一个队列,进程间的 IPC 消息按照先进先出的顺序依次传递,因此不一样的 IPC 消息之间有前后次序的依赖。相比之下,Mojo 则为每个接口建立了一个独立的消息管道,确保不一样接口的 IPC 是独立的。并且为接口的建立独立的消息管道的代价也并不昂贵,只需分配少许的堆内存。
Mojo 的架构设计:
咱们能够看一下 Chrome 引入 Mojo 以后的架构变化。
以前:
以后:
是否是有点微服务的感受。
熟悉 Java 的 Spring 的能够明显看出这个依赖倒置。Blink 原本是浏览器最底层的排版引擎,经过 Mojo,Blink 变成了要给中间模块。最近大热的 Flutter 也是基于 Mojo 架构的。
deno 的介绍是一个安全的 TypeScript 运行环境。可是咱们看源码就会发现,deno 集成进了一个 TypeScript 编译器,而入口文件中 ry/deno:main.go
// It's up to library users to call
// deno.Eval("deno_main.js", "denoMain()")
func Eval(filename string, code string) {
err := worker.Load(filename, code)
exitOnError(err)
} // It's up to library users to call
// deno.Eval("deno_main.js", "denoMain()")
func Eval(filename string, code string) {
err := worker.Load(filename, code)
exitOnError(err)
}
复制代码
使用 V8 运行的 deno_main.js 文件。是 JavaScript 而不是 TypeScript 。
在前面的分析中咱们知道这会影响 deno 的初次启动速度。那么对于执行速度呢?从理论上,TypeScript 做为一种静态类型语言,编译完成的 JavaScript 代码会有更快的执行速度。我以前在《前端程序员应该懂点V8 知识》曾经提到过 V8 对于 JavaScript 性能提高有一项是 Type feedback。
当 V8 执行一个函数时,会基于函数传入的实参(注意是实参,而不是形参,由于 JavaScript 的形参是没有类型的)进行即时编译(JIT):
可是当后面再次以不一样的类型调用函数时,V8 会进行**去优化(Deopt)**操做。
(将以前优化完的结果去掉,称为“去优化”)
可是若是咱们使用 TypeScript ,全部的参数都是由类型标注的,所以能够防止 V8 引擎内部执行去优化操做。
虽然 TypeScript 能够避免 V8 引擎的去优化操做,可是 V8 执行的是 ts 编译后的结果,咱们经过字节码或者机器码能够看到,V8 依然生成了 Type Check 的代码,每次调用函数以前,V8 都会对实参的类型进行检查。也就是说,虽然 TypeScript 保证了函数的参数类型,可是编译成 JavaScript 以后,V8 并不能肯定函数的参数类型,只能经过每次调用前的检查来保证参数的类型。
其次,当 V8 遇到函数定义时,并不知道参数的类型,而只有函数被调用后,V8 才能判断函数的类型,才对函数进行 Typed 即时编译。这里又有一个矛盾了,typescript 在函数定义时就已经知道了形参的类型,而 V8 只有在函数调用时才根据实参的类型进行优化。
因此,目前 deno 的架构还存在不少问题,毕竟只是一个 demo。将来还有不少方向能够优化。
V8 是一个 JavaScript 运行时,而 deno 若是定义为“安全的 TypeScript 运行时”,至少在目前的架构上,性能是有很大损失的。可是目前还不存在一个 TypeScript 运行时,退而求其次只能在 V8 前面放一个 TypeScript 编译器了。
执行流程是这样的:
虽然我在项目中没有使用过 TypeScript ,可是基本上我在项目里面写的第三方库都会提供一d.ts 文件。目前 TypeScript 最大的用途仍是体如今开发和维护过程当中。
咱们想到的一个方式就是 fork 一份 V8 的源码,而后把编译流程整合进去。TypeScript 在编译为 JavaScript 的过程当中也须要一份 AST,而后生成 js 代码。V8 执行 js 代码是再 parse 一份 AST,基于 AST 生成中间代码(ByteCode)。若是 TypeScript 能够直接生成对用的字节码则会提高运行时的性能。
不过 Ryan Dahl 大概不会这么干。可是也未必,毕竟社区已经把 TypeScript 的一个子集编译为 WebAssembly 了。
以前微软的 JScript 和 VBScript 在和 JavaScript 的竞争中败下阵来,而如今 TypeScript 势头正猛。虽然对 ES 规范的兼容束缚了 TypeScript 的发展,但很期待微软能够提供一个 TS 运行时,或者在 Chakra 引擎增长对 TS 运行时的支持。
不论如何,deno 是一个很是伟大的项目,但却不是“下一代 Node.js”。
PS:昨天 Ryan Dahl 在 JS Conf 作了《Design Mistakes in Node》的演讲,目前只有 PPT,尚未 Youtube 视频。而 8 年前的 2009 年,Ryan Dahl 也在 JS Conf 作了一次演讲,此次演讲诞生了 Node.js。