标签 Javascript 下的文章

值得好好学习的 6 个 JavaScript 框架

常言道,条条大路通罗马,可是那一条适合我呢? 由于用于构建前端页面等现代技术的出现,JavaScript 在 Web 开发社区早已是如雷贯耳。通过在网页上编写几个函数并提供执行逻辑,可以很好的支持 HTML (主要是用于页面的 表现 或者 布局)。如果没有 JavaScript,那页面将没有任何 交互特性 可言。

现在的框架和库已经从蛮荒时代崛起了,很多老旧的技术纷纷开始将功能分离成模块。现在不再需要在整个核心语言中支持所有特性了,开发者允许所有用户创建库和框架来增强核心语言的功能。这样,语言的灵活性获得了显著提高。

如果在已经在使用 JavaScript (以及 JQuery) 来支持 HTML,那么你肯定知道开发和维护一个大型应用需要付出多大的努力以及编写多么复杂的代码,而 JavaScript 框架可以帮助你快速的构建交互式 Web 应用 (包含单页面应用或者多页面应用)。

当一个新手开发者想要学习 JavaScript 时,他常常会被各种 JavaScript 框架所吸引,也幸亏有为数众多的社区,任何开发者都可以轻易地通过在线教程或者其他资源来学习。

但是,唉!多数的程序员都很难决定学习和使用哪一个框架。因此在本文中,我将为大家推荐 6 个值得好好学习的 JavaScript 框架。让我们开始吧。

1、AngularJS

Angular

(注:这是我个人最喜欢的框架)

无论你是何时听说的 JavaScript,很可能你早就听过 AngularJS,因为这是在 JavaScript 社区中最为广泛使用的框架了。它发布于 2009 年,由 Google 开发 (这够有说服力让你使用了吧) ,它是一个开源项目,这意味着你可以阅读、编辑和修改其源代码以便更加符合自身的需求,并且不用向其开发者支付一分钱 (这不是很酷吗?)。

如果说你觉得通过纯粹的 JavaScript 代码编写一个复杂的 Web 应用比较困难的话,那么你肯定会兴奋的跳起来,因为它将显著地减轻你的编码负担。它符合支持双向数据绑定的 MVC ( 模型-视图-控制 Model–view–controller ) 设计典范。假如你不熟悉 MVC,你只需要知道它代表着无论何时探测到某些变化,它将自动更新前端 (比如,用户界面端) 和后端 (代码或者服务器端) 数据。

MVC 可以大大减少构建复杂应用程序所需的时间和精力,所以你只需要集中精力于一处即可 (DOM 编程接口会自动同步更新视图和模型)。由于 视图组件模型组件 是分离的,你可以很容易的创建一个可复用的组件,使得用户界面的效果非常好看。

如果因为某些原因,你已经使用了 TypeScript (一种与 JavaScript 非常相似的语言),那么你可以很容易就上手 AngularJS,因为这两者的语法高度相似。与 TypeScript 相似这一特点在一定程度上提升了 AngularJS 的受欢迎程度。

目前,Angular 2.0 已经发布,并且提升了移动端的性能,这也足以向一个新的开发者证明,该框架的开发活跃度够高并且定期更新。

AngularJS 有着大量的用户,包括 (但不限于) Udemy、Forbes、GoDaddy、Ford、NBA 和 Oscars。

对于那些想要一个高效的 MVC 框架,用来开发面面俱到、包含健壮且现代化的基础架构的单页应用的用户来说,我极力的推荐这个框架。这是为无经验 JavaScript 开发者设计的首选框架。

2、React

React

与 AngularJS 相似,React 也是一个 MVC ( 模型-视图-控制 Model–view–controller ) 类型的框架,但不同的是,它完全针对于 视图组件 (因为它是为 UI 特别定制的) ,并且可与任何架构进行无缝衔接。这意味着你可以马上将它运用到你的网站中去。

它从核心功能中抽象出 DOM 编程接口 (并且因此使用了虚拟 DOM),所以你可以快速渲染 UI,这使得你能够通过 node.js 将它作为一个客户端框架来使用。它是由 Facebook 开发的开源项目,还有其它的开发者为它贡献了代码。

假如说你见到过并喜欢 Facebook 和 Instagram 的界面,那么你将会爱上 React。通过 React,你可以给你的应用的每个状态设计一个简单的视图,当数据改变的时候,视图也自动随之改变。只要你想的话,可以创建各种的复杂 UI,也可以在任何应用中复用它。在服务器端,React 同样支持通过 node.js 来进行渲染。对于其他的接口,React 也一样表现得足够灵活。

除 Facebook 和 Instagram 外,还有好多公司也在使用 React,包括 Whatsapp、BBC、PayPal、Netflix 和 Dropbox 等。

如果你只需要一个前端开发框架来构建一个非常复杂且界面极好的强大视图层,那我极力向你推荐这个框架,但你需要有足够的经验来处理各种类型的 JavaScript 代码,而且你再也不需要其他的组件了 (因为你可以自己集成它们)。

3、Ember

Ember

这个 JavaScript 框架在 2011 年正式发布,是由 Yehuda Katz 开发的开源项目。它有一个庞大且活跃的在线社区,所以在有任何问题时,你都可以在社区中提问。该框架吸收融合了非常多的通用的 JavaScript 风格和经验,以便确保开发者能最快地做到开箱即用。

它使用了 MVVM ( 模型-视图-视图模型 Model–view–viewmodel ) 的设计模式,这使得它与 MVC 有些不一样,因为它由一个 连接器 (binder) 帮助视图和数据连接器进行通信。

对于 DOM 编程接口的快速服务端渲染,它借助了 Fastboot.js,这能够让那些复杂 UI 的性能得到极大提高。

它的现代化路由模式和模型引擎还支持 RESTful API,这确保你可以使用到这种最新的技术。它支持 句柄集成模板 Handlebars integrated template ,用来自动更新数据。

早在 2015 年间,它的风头曾一度盖过 AngularJS 和 React,被称为最好的 JavaScript 框架,对于它在 JavaScript 社区中的可用性和吸引力,这样的说服力该是足够了的。

对于不追求高灵活性和大型架构的用户,并且仅仅只是为了赶赴工期、完成任务的话,我个人非常推荐这个 JavaScript 框架。

4、Adonis

Adonis

如果你曾使用过 LaravelNodeJS,那么你在使用这一个框架之时会觉得相当顺手,因为它是集合了这两个平台的优点而形成的一个框架,对于任何种类的现代应用来说,它都显得非常专业、圆润和精致。

它使用了 NodeJS,所以是一个很好的后端框架,同时还附带有一些前端特性 (与前面提到那些更多地注重前端的框架不同),所以想要进入后端开发的新手开发者会发觉这个框架相当迷人。

相比于 NoSQL,很多的开发者都比较喜欢使用 SQL 数据库 (因为他们需要增强和数据以及其它特性的交互性),这一现象在这个框架中得到了很好的体现,这使得它更接近标准,开发者也更容易使用。

如果你混迹于各类 PHP 社区,那你一定很熟悉 服务提供者 Service Providers ,也由于 Adonis 其中包含相应的 PHP 风格,所以在使用它的时候,你会觉得似曾相识。

在它所有的特性中,最好的便是那个极为强大的路由引擎,支持使用函数来组织和管理应用的所有状态、支持错误处理机制、支持通过 SQL ORM 来进行数据库查询、支持生成器、支持 箭头函数 arrow functions 、支持代理等等。

如果喜欢使用无状态 REST API 来构建服务器端应用,我比较推荐它,你会爱上这个框架的。

5、Vue.js

Vue.js

这是一个开源的 JavaScript 框架,发布于 2014 年,它有个极为简单的 API,用来为 现代 Web 界面 Modern Web Interface 开发 交互式组件 Reactive components 。其设计着重于简单易用。与 Ember 相似,它使用的是 MVVM ( 模型-视图-视图模型 Model–view–viewmodel ) 设计范例,这样简化了设计。

这个框架最有吸引力的一点是,你可以根据自身需求来选择使用的模块。比如,你需要编写简单的 HTML 代码,抓取 JSON,然后创建一个 Vue 实例来完成可以复用的小特效。

与之前的那些 JavaScript 框架相似,它使用双路数据绑定来更新模型和视图,同时也使用连接器来完成视图和数据连接器的通信。这是一个还未完全成熟的框架,因为它全部的关注点都在视图层,所以你需要自己处理其它组件。

如果你熟悉 AngularJS,那你会感觉很顺手,因为它大量嵌入了 AngularJS 的架构,如果你懂得 JavaScript 的基础用法,那你的许多项目都可以轻易地迁移到该框架之下。

假如你只想把任务完成,或者想提升你自身的 JavaScript 编程经验,又或者你需要学习不同的 JavAScript 框架的本质,我极力推荐这个。

6、Backbone.js

Backbone.JS

这个框架可以很容易的集成到任何第三方的模板引擎中,默认使用的是 Underscore 模板引擎,而且该框架仅有一个依赖项 (JQuery),因此它以轻量而闻名。它支持带有 RESTful JSON 接口的 MVC ( 模型-视图-控制 Model–view–controller ) (可以自动更新前端和后端) 设计范例。

假如你曾经使用过著名的社交新闻网络服务 reddit,那么你肯定听说过它在几个单页面应用中使用了 Backbone.jsBackbone.js 的原作者为之建立了与 CoffeScript 旗鼓相当的 Underscore 模板引擎,所以你可以放心,开发者知道该做什么。

该框架在一个软件包中提供了 键值对 key-value 模型、视图以及几个打包的模块,所以你不需要额外下载其他的外部包,这样可以节省不少时间。框架的源码可以在 GitHub 进行查看,你可以根据需求进行深度定制。

如果你在寻找一个入门级框架来快速构建一个单页面应用,那么这个框架非常适合你。

总而言之

至此,我已经在本文着重说明了 6 个值得好好学习的 JavaScript 框架,希望你读完本文后能够决定使用哪个框架来完成自己的任务。

如果说对于选择框架,你还是不知所措,请记住,这个世界是实践出真知而非教条主义的。最好就是从列表中挑选一个来使用,看看最后是否满足你的需求和兴趣,如果还是不行,接着试试另一个。你也尽管放心好了,列表中的框架肯定是足够了的。


译者简介:

GHLandy —— 生活中所有欢乐与苦闷都应藏在心中,有些事儿注定无人知晓,自己也无从说起。


via: http://www.discoversdk.com/blog/6-best-javascript-frameworks-to-learn-in-2016

作者:Danyal Zia 译者:GHLandy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是 JavaScript 框架系列的第二章。在这一章里,我打算讲一下在浏览器里的异步代码不同执行方式。你将了解定时器和事件循环之间的不同差异,比如 setTimeout 和 Promises。

这个系列是关于一个开源的客户端框架,叫做 NX。在这个系列里,我主要解释一下写该框架不得不克服的主要困难。如果你对 NX 感兴趣可以参观我们的 主页

这个系列包含以下几个章节:

  1. 项目结构
  2. 定时执行 (当前章节)
  3. 沙箱代码评估
  4. 数据绑定介绍
  5. 数据绑定与 ES6 代理
  6. 自定义元素
  7. 客户端路由

异步代码执行

你可能比较熟悉 Promiseprocess.nextTick()setTimeout(),或许还有 requestAnimationFrame() 这些异步执行代码的方式。它们内部都使用了事件循环,但是它们在精确计时方面有一些不同。

在这一章里,我将解释它们之间的不同,然后给大家演示怎样在一个类似 NX 这样的先进框架里面实现一个定时系统。不用我们重新做一个,我们将使用原生的事件循环来达到我们的目的。

事件循环

事件循环甚至没有在 ES6 规范里提到。JavaScript 自身只有任务(Job)和任务队列(job queue)。更加复杂的事件循环是在 NodeJS 和 HTML5 规范里分别定义的,因为这篇是针对前端的,我会在详细说明后者。

事件循环可以被看做某个条件的循环。它不停的寻找新的任务来运行。这个循环中的一次迭代叫做一个滴答(tick)。在一次滴答期间执行的代码称为一次任务(task)。

while (eventLoop.waitForTask()) {  
  eventLoop.processNextTask()
}

任务是同步代码,它可以在循环中调度其它任务。一个简单的调用新任务的方式是 setTimeout(taskFn)。不管怎样, 任务可能有很多来源,比如用户事件、网络或者 DOM 操作。

任务队列

更复杂一些的是,事件循环可以有多个任务队列。这里有两个约束条件,相同任务源的事件必须在相同的队列,以及任务必须按插入的顺序进行处理。除此之外,浏览器可以做任何它想做的事情。例如,它可以决定接下来处理哪个任务队列。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
}

用这个模型,我们不能精确的控制定时。如果用 setTimeout()浏览器可能决定先运行完其它几个队列才运行我们的队列。

微任务队列

幸运的是,事件循环还提供了一个叫做微任务(microtask)队列的单一队列。当前任务结束的时候,微任务队列会清空每个滴答里的任务。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

最简单的调用微任务的方法是 Promise.resolve().then(microtaskFn)。微任务按照插入顺序进行处理,并且由于仅存在一个微任务队列,浏览器不会把时间弄乱了。

此外,微任务可以调度新的微任务,它将插入到同一个队列,并在同一个滴答内处理。

绘制 Rendering

最后是 绘制 Rendering 调度,不同于事件处理和分解,绘制并不是在单独的后台任务完成的。它是一个可以运行在每个循环滴答结束时的算法。

在这里浏览器又有了许多自由:它可能在每个任务以后绘制,但是它也可能在好几百个任务都执行了以后也不绘制。

幸运的是,我们有 requestAnimationFrame(),它在下一个绘制之前执行传递的函数。我们最终的事件模型像这样:

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }

  if (shouldRender()) {
    applyScrollResizeAndCSS()
    runAnimationFrames()
    render()
  }
}

现在用我们所知道知识来创建定时系统!

利用事件循环

和大多数现代框架一样,NX 也是基于 DOM 操作和数据绑定的。批量操作和异步执行以取得更好的性能表现。基于以上理由我们用 PromisesMutationObserversrequestAnimationFrame()

我们所期望的定时器是这样的:

  1. 代码来自于开发者
  2. 数据绑定和 DOM 操作由 NX 来执行
  3. 开发者定义事件钩子
  4. 浏览器进行绘制

步骤 1

NX 寄存器对象基于 ES6 代理 以及 DOM 变动基于MutationObserver (变动观测器)同步运行(下一节详细介绍)。 它作为一个微任务延迟直到步骤 2 执行以后才做出反应。这个延迟已经在 Promise.resolve().then(reaction) 进行了对象转换,并且它将通过变动观测器自动运行。

步骤 2

来自开发者的代码(任务)运行完成。微任务由 NX 开始执行所注册。 因为它们是微任务,所以按序执行。注意,我们仍然在同一个滴答循环中。

步骤 3

开发者通过 requestAnimationFrame(hook) 通知 NX 运行钩子。这可能在滴答循环后发生。重要的是,钩子运行在下一次绘制之前和所有数据操作之后,并且 DOM 和 CSS 改变都已经完成。

步骤 4

浏览器绘制下一个视图。这也有可能发生在滴答循环之后,但是绝对不会发生在一个滴答的步骤 3 之前。

牢记在心里的事情

我们在原生的事件循环之上实现了一个简单而有效的定时系统。理论上讲它运行的很好,但是还是很脆弱,一个轻微的错误可能会导致很严重的 BUG。

在一个复杂的系统当中,最重要的就是建立一定的规则并在以后保持它们。在 NX 中有以下规则:

  1. 永远不用 setTimeout(fn, 0) 来进行内部操作
  2. 用相同的方法来注册微任务
  3. 微任务仅供内部操作
  4. 不要干预开发者钩子运行时间

规则 1 和 2

数据反射和 DOM 操作将按照操作顺序执行。这样只要不混合就可以很好的延迟它们的执行。混合执行会出现莫名其妙的问题。

setTimeout(fn, 0) 的行为完全不可预测。使用不同的方法注册微任务也会发生混乱。例如,下面的例子中 microtask2 不会正确地在 microtask1 之前运行。

Promise.resolve().then().then(microtask1)  
Promise.resolve().then(microtask2) 

规则 3 和 4

分离开发者的代码执行和内部操作的时间窗口是非常重要的。混合这两种行为会导致不可预测的事情发生,并且它会需要开发者了解框架内部。我想很多前台开发者已经有过类似经历。

结论

如果你对 NX 框架感兴趣,可以参观我们的主页。还可以在 GIT 上找到我们的源代码

在下一节我们再见,我们将讨论 沙盒化代码执行

你也可以给我们留言。


via: https://blog.risingstack.com/writing-a-javascript-framework-execution-timing-beyond-settimeout/

作者:Bertalan Miklos 译者:kokialoves 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

更新(2016/10/30):我写完这篇文章之后,我在这个基准测试中发了一个错误,会导致 Rollup 比它预期的看起来要好一些。不过,整体结果并没有明显的不同(Rollup 仍然击败了 Browserify 和 Webpack,虽然它并没有像 Closure 十分好),所以我只是更新了图表。该基准测试包括了 RequireJS 和 RequireJS Almond 打包器,所以文章中现在也包括了它们。要看原始帖子,可以查看历史版本

大约一年之前,我在将一个大型 JavaScript 代码库重构为更小的模块时发现了 Browserify 和 Webpack 中一个令人沮丧的事实:

“代码越模块化,代码体积就越大。:< ”

  • Nolan Lawson

过了一段时间,Sam Saccone 发布了一些关于 TumblrImgur 页面加载性能的出色的研究。其中指出:

“超过 400 ms 的时间单纯的花费在了遍历 Browserify 树上。”

  • Sam Saccone

在本篇文章中,我将演示小模块可能会根据你选择的 打包器 bundler 模块系统 module system 而出现高得惊人的性能开销。此外,我还将解释为什么这种方法不但影响你自己代码的模块,也会影响依赖项中的模块,这也正是第三方代码在性能开销上很少提及的方面。

网页性能

一个页面中包含的 JavaScript 脚本越多,页面加载也将越慢。庞大的 JavaScript 包会导致浏览器花费更多的时间去下载、解析和执行,这些都将加长载入时间。

即使当你使用如 Webpack code splitting、Browserify factor bundles 等工具将代码分解为多个包,该开销也仅仅是被延迟到页面生命周期的晚些时候。JavaScript 迟早都将有一笔开销。

此外,由于 JavaScript 是一门动态语言,同时流行的 CommonJS 模块也是动态的,所以这就使得在最终分发给用户的代码中剔除无用的代码变得异常困难。譬如你可能只使用到 jQuery 中的 $.ajax,但是通过载入 jQuery 包,你将付出整个包的代价。

JavaScript 社区对这个问题提出的解决办法是提倡 小模块 的使用。小模块不仅有许多 美好且实用的好处 如易于维护,易于理解,易于集成等,而且还可以通过鼓励包含小巧的功能而不是庞大的库来解决之前提到的 jQuery 的问题。

所以在小模块下,你将不需要这样:

var _ = require('lodash')
_.uniq([1,2,2,3])

而是可以如此:

var uniq = require('lodash.uniq')
uniq([1,2,2,3])

包与模块

需要强调的是这里我提到的“模块”并不同于 npm 中的“包”的概念。当你从 npm 安装一个包时,它会将该模块通过公用 API 展现出来,但是在这之下其实是一个许多模块的聚合物。

例如,我们来看一个包 is-array,它没有别的依赖,并且只包含 一个 JavaScript 文件,所以它只有一个模块。这算是足够简单的。

现在来看一个稍微复杂一点的包,如 once。它有一个依赖的包 wrappy 包都各自包含一个模块,所以总模块数为 2。至此,也还算好。

现在来一起看一个更为令人迷惑的例子:qs。因为它没有依赖的包,所以你可能就认为它只有一个模块,然而事实上,它有四个模块!

你可以用一个我写的工具 browserify-count-modules 来统计一个 Browserify 包的总模块数:

$ npm install qs
$ browserify node_modules/qs | browserify-count-modules
4

这说明了一个包可以包含一个或者多个模块。这些模块也可以依赖于其他的包,而这些包又将附带其自己所依赖的包与模块。由此可以确定的事就是任何一个包将包含至少一个模块。

模块膨胀

一个典型的网页应用中会包含多少个模块呢?我在一些流行的使用 Browserify 的网站上运行 browserify-count-moduleson 并且得到了以下结果:

顺带一提,我写过的最大的开源站点 Pokedex.org 包含了 4 个包,共 311 个模块。

让我们先暂时忽略这些 JavaScript 包的实际大小,我认为去探索一下一定数量的模块本身开销会是一件有意思的事。虽然 Sam Saccone 的文章 “2016 年 ES2015 转译的开销” 已经广为流传,但是我认为他的结论还未到达足够深度,所以让我们挖掘的稍微再深一点吧。

测试环节!

我构造了一个能导入 100、1000 和 5000 个其他小模块的测试模块,其中每个小模块仅仅导出一个数字。而父模块则将这些数字求和并记录结果:

// index.js
var total = 0
total += require('./module_0')
total += require('./module_1')
total += require('./module_2')
// etc.
console.log(total)


// module_1.js
module.exports = 1

我测试了五种打包方法:Browserify、带 bundle-collapser 插件的 Browserify、Webpack、Rollup 和 Closure Compiler。对于 Rollup 和 Closure Compiler 我使用了 ES6 模块,而对于 Browserify 和 Webpack 则用的是 CommonJS,目的是为了不涉及其各自缺点而导致测试的不公平(由于它们可能需要做一些转译工作,如 Babel 一样,而这些工作将会增加其自身的运行时间)。

为了更好地模拟一个生产环境,我对所有的包采用带 -mangle-compress 参数的 Uglify ,并且使用 gzip 压缩后通过 GitHub Pages 用 HTTPS 协议进行传输。对于每个包,我一共下载并执行 15 次,然后取其平均值,并使用 performance.now() 函数来记录载入时间(未使用缓存)与执行时间。

包大小

在我们查看测试结果之前,我们有必要先来看一眼我们要测试的包文件。以下是每个包最小处理后但并未使用 gzip 压缩时的体积大小(单位:Byte):

100 个模块1000 个模块5000 个模块
browserify798279987419985
browserify-collapsed578657991309982
webpack395439055203052
rollup671697138968
closure758795843955
100 个模块1000 个模块5000 个模块
browserify16491380064513
browserify-collapsed14641190356335
webpack693502726363
rollup300214511510
closure302214011789

Browserify 和 Webpack 的工作方式是隔离各个模块到各自的函数空间,然后声明一个全局载入器,并在每次 require() 函数调用时定位到正确的模块处。下面是我们的 Browserify 包的样子:

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o

而 Rollup 和 Closure 包看上去则更像你亲手写的一个大模块。这是 Rollup 打包的包:

(function () {
        'use strict';
        var total = 0
        total += 0
        total += 1
        total += 2
// etc.

如果你清楚在 JavaScript 中使用嵌套函数与在关联数组查找一个值的固有开销, 那么你将很容易理解出现以下测试的结果的原因。

测试结果

我选择在搭载 Android 5.1.1 与 Chrome 52 的 Nexus 5(代表中低端设备)和运行 iOS 9 的第 6 代 iPod Touch(代表高端设备)上进行测试。

这是 Nexus 5 下的测试结果(查看表格):

Nexus 5 结果

这是 iPod Touch 下的测试结果(查看表格):

iPod Touch 结果

在 100 个模块时,各包的差异是微不足道的,但是一旦模块数量达到 1000 个甚至 5000 个时,差异将会变得非常巨大。iPod Touch 在不同包上的差异并不明显,而对于具有一定年代的 Nexus 5 来说,Browserify 和 Webpack 明显耗时更多。

与此同时,我发现有意思的是 Rollup 和 Closure 的运行开销对于 iPod 而言几乎可以忽略,并且与模块的数量关系也不大。而对于 Nexus 5 来说,运行的开销并非完全可以忽略,但 Rollup/Closure 仍比 Browserify/Webpack 低很多。后者若未在几百毫秒内完成加载则将会占用主线程的好几帧的时间,这就意味着用户界面将冻结并且等待直到模块载入完成。

值得注意的是前面这些测试都是在千兆网速下进行的,所以在网络情况来看,这只是一个最理想的状况。借助 Chrome 开发者工具,我们可以认为地将 Nexus 5 的网速限制到 3G 水平,然后来看一眼这对测试产生的影响(查看表格):

Nexus 5 3G 结果

一旦我们将网速考虑进来,Browserify/Webpack 和 Rollup/Closure 的差异将变得更为显著。在 1000 个模块规模(接近于 Reddit 1050 个模块的规模)时,Browserify 花费的时间比 Rollup 长大约 400 毫秒。然而 400 毫秒已经不是一个小数目了,正如 Google 和 Bing 指出的,亚秒级的延迟都会 对用户的参与产生明显的影响

还有一件事需要指出,那就是这个测试并非测量 100 个、1000 个或者 5000 个模块的每个模块的精确运行时间。因为这还与你对 require() 函数的使用有关。在这些包中,我采用的是对每个模块调用一次 require() 函数。但如果你每个模块调用了多次 require() 函数(这在代码库中非常常见)或者你多次动态调用 require() 函数(例如在子函数中调用 require() 函数),那么你将发现明显的性能退化。

Reddit 的移动站点就是一个很好的例子。虽然该站点有 1050 个模块,但是我测量了它们使用 Browserify 的实际执行时间后发现比“1000 个模块”的测试结果差好多。当使用那台运行 Chrome 的 Nexus 5 时,我测出 Reddit 的 Browserify require() 函数耗时 2.14 秒。而那个“1000 个模块”脚本中的等效函数只需要 197 毫秒(在搭载 i7 处理器的 Surface Book 上的桌面版 Chrome,我测出的结果分别为 559 毫秒与 37 毫秒,虽然给出桌面平台的结果有些令人惊讶)。

这结果提示我们有必要对每个模块使用多个 require() 函数的情况再进行一次测试。不过,我并不认为这对 Browserify 和 Webpack 会是一个公平的测试,因为 Rollup 和 Closure 都会将重复的 ES6 库导入处理为一个的顶级变量声明,同时也阻止了顶层空间以外的其他区域的导入。所以根本上来说,Rollup 和 Closure 中一个导入和多个导入的开销是相同的,而对于 Browserify 和 Webpack,运行开销随 require() 函数的数量线性增长。

为了我们这个分析的目的,我认为最好假设模块的数量是性能的短板。而事实上,“5000 个模块”也是一个比“5000 个 require() 函数调用”更好的度量标准。

结论

首先,bundle-collapser 对 Browserify 来说是一个非常有用的插件。如果你在产品中还没使用它,那么你的包将相对来说会略大且运行略慢(虽然我得承认这之间的差异非常小)。另一方面,你还可以转换到 Webpack 以获得更快的包而不需要额外的配置(其实我非常不愿意这么说,因为我是个顽固的 Browserify 粉)。

不管怎样,这些结果都明确地指出 Webpack 和 Browserify 相较 Rollup 和 Closure Compiler 而言表现都稍差,并且性能差异随着模块大小的增大而增大。不幸的是,我并不确定 Webpack 2 是否能解决这些问题,因为尽管他们将 从 Rollup 中借鉴一些想法,但是看起来他们的关注点更多在于 tree-shaking 方面 而不是在于 scope-hoisting 方面。(更新:一个更好的名字称为 内联 inlining ,并且 Webpack 团队 正在做这方面的工作。)

给出这些结果之后,我对 Closure Compiler 和 Rollup 在 JavaScript 社区并没有得到过多关注而感到惊讶。我猜测或许是因为(前者)需要依赖 Java,而(后者)仍然相当不成熟并且未能做到开箱即用(详见 Calvin’s Metcalf 的评论 中作的不错的总结)。

即使没有足够数量的 JavaScript 开发者加入到 Rollup 或 Closure 的队伍中,我认为 npm 包作者们也已准备好了去帮助解决这些问题。如果你使用 npm 安装 lodash,你将会发其现主要的导入是一个巨大的 JavaScript 模块,而不是你期望的 Lodash 的 超模块 hyper-modular 特性(require('lodash/uniq')require('lodash.uniq') 等等)。对于 PouchDB,我们做了一个类似的声明以 使用 Rollup 作为预发布步骤,这将产生对于用户而言尽可能小的包。

同时,我创建了 rollupify 来尝试将这过程变得更为简单一些,只需拖动到已存在的 Browserify 工程中即可。其基本思想是在你自己的项目中使用 导入 import 导出 export (可以使用 cjs-to-es6 来帮助迁移),然后使用 require() 函数来载入第三方包。这样一来,你依旧可以在你自己的代码库中享受所有模块化的优点,同时能导出一个适当大小的大模块来发布给你的用户。不幸的是,你依旧得为第三方库付出一些代价,但是我发现这是对于当前 npm 生态系统的一个很好的折中方案。

所以结论如下:一个大的 JavaScript 包比一百个小 JavaScript 模块要快。尽管这是事实,我依旧希望我们社区能最终发现我们所处的困境————提倡小模块的原则对开发者有利,但是对用户不利。同时希望能优化我们的工具,使得我们可以对两方面都有利。

福利时间!三款桌面浏览器

通常来说我喜欢在移动设备上运行性能测试,因为在这里我们能更清楚的看到差异。但是出于好奇,我也分别在一台搭载 i7 的 Surface Book 上的 Chrome 52、Edge 14 和 Firefox 48 上运行了测试。这分别是它们的测试结果:

Chrome 52 (查看表格)

Chrome 结果

Edge 14 (查看表格)

Edge 结果

Firefox 48 (查看表格)

Firefox 结果

我在这些结果中发现的有趣的地方如下:

  1. bundle-collapser 总是与 slam-dunk 完全不同。
  2. Rollup 和 Closure 的下载时间与运行时间之比总是非常高,它们的运行时间基本上微不足道。ChakraCore 和 SpiderMonkey 运行最快,V8 紧随其后。

如果你的 JavaScript 非常大并且是延迟加载,那么第二点将非常重要。因为如果你可以接受等待网络下载的时间,那么使用 Rollup 和 Closure 将会有避免界面线程冻结的优点。也就是说,它们将比 Browserify 和 Webpack 更少出现界面阻塞。

更新:在这篇文章的回应中,JDD 已经 给 Webpack 提交了一个 issue。还有 一个是给 Browserify 的

更新 2:Ryan Fitzer 慷慨地增加了 RequireJS 和包含 Almond 的 RequireJS 的测试结果,两者都是使用 AMD 而不是 CommonJS 或者 ES6。

测试结果表明 RequireJS 具有 最大的包大小 但是令人惊讶的是它的运行开销 与 Rollup 和 Closure 非常接近。这是在运行 Chrome 52 的 Nexus 5 下限制网速为 3G 的测试结果:

Nexus 5 (3G) RequireJS 结果

更新 3: 我写了一个 optimize-js ,它会减少一些函数内的函数的解析成本。


via: https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/

作者:Nolan 译者:Yinr 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux 基金会宣布成立了 JS 基金会 JS Foundation ,该基金会是一个包容性项目,用于促进指导 JavaScript 生态中各种核心开源项目的发展,其主要任务是管理和资助这些项目,以及培育 JavaScript 生态。

Linux 基金会已经对 JS 生态进行了大量投资

Node.js 社区内部分裂之后,Node.js 代码分成了两个不同的项目:io.js 和 Node.js。后来两个项目和好之后,在 2015 年, Linux 基金会帮助创立了 Node.js 基金会,两个项目合并代码并归于统一的 Node.js 基金会。

JS 基金会也遵循 Node.js 基金会的模式,基金会成员将会支持各种 JavaScript 项目,包括技术指导、人力资源和资金。

IBM 和三星也是基金会成员

基金会的成员包括 Bocoup、IBM、Ripple、Samsung、Sauce Labs、Sense Tecnic Systems、SitePen、StackPath、University of Westminster 和 WebsiteSetup 等。

初始包括的项目有:Appium、 Chassis、 Dojo Toolkit、 ESLint、 Esprima、 Globalize、 Grunt、 Interledger.js、 Intern、 Jed、 JerryScript、 jQuery、 jQuery Mobile、 jQuery UI、 Lodash、 Mocha、 Moment、 Node-RED、 PEP、 QUnit、 RequireJS、 Sizzle 和 webpack。

这些大多数都是顶级的 JavaScript 项目,被成千上万的开发者们所使用。然而我们注意到, Node.js 项目由于有自己的基金会,并没有加入到 JS 基金会之中。

如果同意加入 JS 基金会,这些项目将会从公共开发战略中受益,以消除当今 JavaScript 社区中的混乱状况。基金会会设立所有项目共同遵循的目标,并为核心项目提供资金支持。这些核心项目现今用于服务器端应用、浏览器、云应用和物联网设备。

jQuery 基金会变成了 JS 基金会

JS 基金会并不是凭空建立的,它的核心就是之前的 jQuery 基金会。虽然 jQuery 现在已经发展放缓,甚至很多开发者都惊奇 jQuery 仍然还活着,因为目前已经出现了各种更好的工具。 但是 jQuery 在某种意义上仍然是 JavaScript 的代名词。jQuery 基金会的很多成员在 JavaScript 领域内都很有影响力,所以成立新的 JS 基金会将有助于发挥它们的作用,促进 JavaScript 生态的进一步发展。

TypeScript 是一种基于 JavaScript 衍生的语言,是由微软为了使大型 Web 应用开发更容易而创造的一种语言,现在已经发布了 2.0 里程碑版本

在用于大型开发时, JavaScript 由于其固有的特性而面临一些挑战。其它的静态编译语言,如 C#、Java 和 C++ 在每次开发人员敲下“编译”时会进行全面的错误检查,而 JavaScript 直到运行时才会做错误检查。这意味着,从输入错误到像对非数字进行数学运算这样的错误用法都根本不会遇到检查,所以,用户不走运的话就会遇到这些问题。而在 TypeScript 中,微软的目标是引入一些其它语言也提供的检查和校验,而依然保持和 JavaScript 的兼容性,并可以编译成 JavaScript。

根据介绍,TypeScript 2.0 引入了一些新功能,改进了性能、增强了 JavaScript 兼容性,并在 TypeScript 进行编译时扩大了错误检查的范围。TypeScript 2.0 中的一大进步就是给予开发人员对 null 值的更大控制。

null 用于表示变量根本没有值,它被其发明人戏称为“价值十亿美元的错误”。一次又一次,程序总是由于没有正确检查一个值是否是 null 值而掉到坑里。但是不管好与不好,所有主流的编程语言都支持这个 null 的概念。

TypeScript 2.0 引入了许多新的特性,但是其中最大的特性就是对 null 值的控制。在 TypeScript 2.0 中,开发人员可以可以启用一种新的行为,以默认防止变量赋值为 null。当启用该选项时,默认情况下变量必须有一个值,且这个值不能是 null。这可以让编译器发现变量没有初始化的错误。

TypeScript 似乎赢得了许多 JavaScript 开发者的拥护,谷歌采用它来开发 Angular 2 框架,而 Visual Studio、Visual Studio Code、Eclipse、Emacs、Vim 等等开发环境也都支持 TypeScript。微软已经把它作为社区驱动的项目进行了开源,目前已经有超过 150 个独立贡献者参与了该项目,这已经是雷蒙德拥抱开源的成功典范之一。

解析器是一种超级有用的软件库。从概念上简单的说,它们的实现很有挑战性,并且在计算机科学中经常被认为是黑魔法。在这个系列的博文中,我会向你们展示为什么你不需要成为哈利波特就能够精通解析器这种魔法。但是为了以防万一带上你的魔杖吧!

我们将探索一种叫做 Ohm 的新的开源库,它使得搭建解析器很简单并且易于重用。在这个系列里,我们使用 Ohm 去识别数字,构建一个计算器等等。在这个系列的最后你将已经用不到 200 行的代码发明了一种完整的编程语言。这个强大的工具将让你能够做到一些你可能过去认为不可能的事情。

为什么解析器很困难?

解析器非常有用。在很多时候你可能需要一个解析器。或许有一种你需要处理的新的文件格式,但还没有人为它写了一个库;又或许你发现了一种古老格式的文件,但是已有的解析器不能在你的平台上构建。我已经看到这样的事发生无数次。 Code 在或者不在, Data 就在那里,不增不减。

从根本上来说,解析器很简单:只是把一个数据结构转化成另一个。所以你会不会觉得你要是邓布利多校长就好了?

解析器历来是出奇地难写,所面临的挑战是绝大多数现有的工具都很老,并且需要一定的晦涩难懂的计算机科学知识。如果你在大学里上过编译器课程,那么课本里也许还有从上世纪七十年传下来的技术。幸运的是,解析器技术从那时候起已经提高了很多。

典型的,解析器是通过使用一种叫作 形式语法 formal grammar 的特殊语法来定义你想要解析的东西来创造的,然后你需要把它放入像 BisonYacc 的工具中,这些工具能够产生一堆 C 代码,这些代码你需要修改或者链接到你实际写入的编程语言中。另外的选择是用你更喜欢的语言亲自动手写一个解析器,这很慢且很容易出错,在你能够真正使用它之前还有许多额外的工作。

想像一下,是否你关于你想要解析的东西的语法描述也是解析器?如果你能够只是直接运行这些语法,然后仅在你需要的地方增加一些 挂钩 hook 呢?那就是 Ohm 所可以做到的事。

Ohm 简介

Ohm 是一种新的解析系统。它类似于你可能已经在课本里面看到过的语法,但是它更强大,使用起来更简单。通过 Ohm, 你能够使用一种灵活的语法在一个 .ohm 文件中来写你自己的格式定义,然后使用你的宿主语言把语义加入到里面。在这篇博文里,我们将用 JavaScript 作为宿主语言。

Ohm 建立于一个为创造更简单、更灵活的解析器的多年研究基础之上。VPRI 的 STEPS program (pdf) 使用 Ohm 的前身 Ometa 为许多特殊的任务创造了专门的语言(比如一个有 400 行代码的平行制图描绘器)。

Ohm 有许多有趣的特点和符号,但是相比于全部解释它们,我认为我们只需要深入其中并构建一些东西就行了。

解析整数

让我们来解析一些数字。这看起来会很简单,只需在一个文本串中寻找毗邻的数字,但是让我们尝试去处理所有形式的数字:整数和浮点数、十六进制数和八进制数、科学计数、负数。解析数字很简单,正确解析却很难。

亲自构建这个代码将会很困难,会有很多问题,会伴随有许多特殊的情况,比如有时会相互矛盾。正则表达式或许可以做的这一点,但是它会非常丑陋而难以维护。让我们用 Ohm 来试试。

用 Ohm 构建的解析器涉及三个部分: 语法 grammar 语义 semantics 测试 tests 。我通常挑选问题的一部分为它写测试,然后构建足够的语法和语义来使测试通过。然后我再挑选问题的另一部分,增加更多的测试、更新语法和语义,从而确保所有的测试能够继续通过。即使我们有了新的强大的工具,写解析器从概念上来说依旧很复杂。测试是用一种合理的方式来构建解析器的唯一方法。现在,让我们开始工作。

我们将从整数开始。一个整数由一系列相互毗邻的数字组成。让我们把下面的内容放入一个叫做 grammar.ohm 的文件中:

CoolNums {
   // just a basic integer
   Number = digit+
}

这创造了一条匹配一个或多个数字(digit)叫作 Number 的单一规则。 意味着一个或更多,就在正则表达式中一样。当有一个或更多的数字时,这个规则将会匹配它们,如果没有数字或者有一些不是数字的东西将不会匹配。“数字(digit)”的定义是从 0 到 9 其中的一个字符。digit 也是像 Number 一样的规则,但是它是 Ohm 的其中一条构建规则因此我们不需要去定义它。如果我们想的话可以推翻它,但在这时候这没有任何意义,毕竟我们不打算去发明一种新的数。

现在,我们可以读入这个语法并用 Ohm 库来运行它。

把它放入 test1.js:

var ohm = require('ohm-js');
var fs = require('fs');
var assert = require('assert');
var grammar = ohm.grammar(fs.readFileSync('src/blog_numbers/syntax1.ohm').toString());

Ohm.grammar 调用将读入该文件并解析成一个语法对象。现在我们可以增加一些语义。把下面内容增加到你的 JavaScript 文件中:

var sem = grammar.createSemantics().addOperation('toJS', {
    Number: function(a) {
        return parseInt(this.sourceString,10);
    }
});

这通过 toJS 操作创造了一个叫作 sem 的语法集。这些语义本质上是一些对应到语法中每个规则的函数。每个函数当与之相匹配的语法规则被解析时将会被调用。上面的 Number 函数将会在语法中的 Number 规则被解析时被调用。 语法 grammar 定义了在语言中这些代码是什么, 语义 semantics 定义了当这些代码被解析时应该做什么。

语义函数能够做我们想做的任何事,比如打印出故障信息、创建对象,或者在任何子节点上递归调用 toJS。此时我们仅仅想把匹配的文本转换成真正的 JavaScript 整数。

所有的语义函数有一个内含的 this 对象,带有一些有用的属性。其 source 属性代表了输入文本中和这个节点相匹配的部分。this.sourceString 是一个匹配输入的串,调用内置在 JavaScript 中的 parseInt 函数会把这个串转换成一个数。传给 parseInt10 这个参数告诉 JavaScript 我们输入的是一个以 10 为基底(10 进制)的数。如果少了这个参数, JavaScript 也会假定以 10 为基底,但是我们把它包含在里面因为后面我们将支持以 16 为基底的数,所以使之明确比较好。

既然我们有一些语法,让我们来实际解析一些东西看一看我们的解析器是否能够工作。如何知道我们的解析器可以工作?通过测试,许多许多的测试,每一个可能的边缘情况都需要一个测试。

使用标准的断言 assert API,以下这个测试函数能够匹配一些输入并运用我们的语义把它转换成一个数,然后把这个数和我们期望的输入进行比较。

   function test(input, answer) {
     var match = grammar.match(input);
     if(match.failed()) return console.log("input failed to match " + input + match.message);     
     var result = sem(match).toJS();
     assert.deepEqual(result,answer);
     console.log('success = ', result, answer);
    }

就是如此。现在我们能够为各种不同的数写一堆测试。如果匹配失败我们的脚本将会抛出一个例外。否则就打印成功信息。让我们尝试一下,把下面这些内容加入到脚本中:

    test("123",123);
    test("999",999);
    test("abc",999);

然后用 node test1.js 运行脚本。

你的输出应该是这样:

success =  123 123
success =  999 999
input failed to match abcLine 1, col 1:
> 1 | abc
      ^
Expected a digit

真酷。正如预期的那样,前两个成功了,第三个失败了。更好的是,Ohm 自动给了我们一个很棒的错误信息指出匹配失败。

浮点数

我们的解析器工作了,但是它做的工作不是很有趣。让我们把它扩展成既能解析整数又能解析浮点数。改变 grammar.ohm 文件使它看起来像下面这样:

CoolNums {
  // just a basic integer
  Number = float | int
  int    = digit+
  float  = digit+ "." digit+
}

这把 Number 规则改变成指向一个浮点数(float)或者一个整数(int)。这个 | 代表着“或”。我们把这个读成“一个 Number 由一个浮点数或者一个整数构成。”然后整数(int)定义成 digit+,浮点数(float)定义成 digit+ 后面跟着一个句号然后再跟着另一个 digit+。这意味着在句号前和句号后都至少要有一个数字。如果一个数中没有一个句号那么它就不是一个浮点数,因此就是一个整数。

现在,让我们再次看一下我们的语义功能。由于我们现在有了新的规则所以我们需要新的功能函数:一个作为整数的,一个作为浮点数的。

var sem = grammar.createSemantics().addOperation('toJS', {
    Number: function(a) {
        return a.toJS();
    },
    int: function(a) {
        console.log("doing int", this.sourceString);
        return parseInt(this.sourceString,10);
    },
    float: function(a,b,c) {
        console.log("doing float", this.sourceString);
        return parseFloat(this.sourceString);
    }
});

这里有两件事情需要注意。首先,整数(int)、浮点数(float)和数(Number)都有相匹配的语法规则和函数。然而,针对 Number 的功能不再有任何意义。它接收子节点 a 然后返回该子节点的 toJS 结果。换句话说,Number 规则简单的返回相匹配的子规则。由于这是在 Ohm 中任何规则的默认行为,因此实际上我们不用去考虑 Number 的作用,Ohm 会替我们做好这件事。

其次,整数(int)有一个参数 a,然而浮点数有三个:abc。这是由于规则的 实参数量 arity 决定的。 实参数量 arity 意味着一个规则里面有多少参数。如果我们回过头去看语法,浮点数(float)的规则是:

  float  = digit+ "." digit+

浮点数规则通过三个部分来定义:第一个 digit+.、以及第二个 digit+。这三个部分都会作为参数传递给浮点数的功能函数。因此浮点数必须有三个参数,否则 Ohm 库会给出一个错误。在这种情况下我们不用在意参数,因为我们仅仅直接攫取了输入串,但是我们仍然需要参数列在那里来避免编译器错误。后面我们将实际使用其中一些参数。

现在我们可以为新的浮点数支持添加更多的测试。

test("123",123);
test("999",999);
//test("abc",999);
test('123.456',123.456);
test('0.123',0.123);
test('.123',0.123);

注意最后一个测试将会失败。一个浮点数必须以一个数开始,即使它就是个 0,.123 不是有效的,实际上真正的 JavaScript 语言也有相同的规则。

十六进制数

现在我们已经有了整数和浮点数,但是还有一些其它的数的语法最好可以支持:十六进制数和科学计数。十六进制数是以 16 为基底的整数。十六进制数的数字能从 0 到 9 和从 A 到 F。十六进制数经常用在计算机科学中,当用二进制数据工作时,你可以仅仅使用两个数字表示 0 到 255 的数。

在绝大多数源自 C 的编程语言(包括 JavaScript),十六进制数通过在前面加上 0x 来向编译器表明后面跟的是一个十六进制数。为了让我们的解析器支持十六进制数,我们只需要添加另一条规则。

  Number = hex | float | int
  int    = digit+
  float  = digit+ "." digit+
  hex    = "0x" hexDigit+
  hexDigit := "0".."9" | "a".."f" | "A".."F"

我实际上已经增加了两条规则。十六进制数(hex)表明它是一个 0x 后面一个或多个十六进制数字(hexDigits)的串。一个十六进制数字(hexDigit)是从 0 到 9,或从 a 到 f,或 A 到 F(包括大写和小写的情况)的一个字符。我也修改了 Number 规则来识别十六进制数作为另外一种可能的情况。现在我们只需要另一条针对十六进制数的功能规则。

    hex: function(a,b) {
        return parseInt(this.sourceString,16);
    }

注意到,在这种情况下,我们把 16 作为基底传递给 parseInt,因为我们希望 JavaScript 知道这是一个十六进制数。

我略过了一些很重要需要注意的事。hexDigit 的规则像下面这样:

  hexDigit := "0".."9" | "a".."f" | "A".."F"

注意我使用的是 := 而不是 =。在 Ohm 中,:= 是当你需要推翻一条规则的时候使用。这表明 Ohm 已经有了一条针对 hexDigit 的默认规则,就像 digitspace 等一堆其他的东西。如果我使用了 =, Ohm 将会报告一个错误。这是一个检查,从而避免我无意识的推翻一个规则。由于新的 hexDigit 规则和 Ohm 的构建规则一样,所以我们可以把它注释掉,然后让 Ohm 自己来实现它。我留下这个规则只是因为这样我们可以看到它实际上是如何进行的。

现在,我们可以添加更多的测试然后看到十六进制数真的能工作:

test('0x456',0x456);
test('0xFF',255);

科学计数

最后,让我们来支持科学计数。科学计数是针对非常大或非常小的数的,比如 1.8×10^3。在大多数编程语言中,科学计数法表示的数会写成这样:1.8e3 表示 18000,或者 1.8e-3 表示 .018。让我们增加另外一对规则来支持这个指数表示:

    float  = digit+ "." digit+ exp?
    exp    = "e" "-"? digit+

上面在浮点数规则末尾增加了一个指数(exp)规则和一个 ?? 表示没有或有一个,所以指数(exp)是可选的,但是不能超过一个。增加指数(exp)规则也改变了浮点数规则的实参数量,所以我们需要为浮点数功能增加另一个参数,即使我们不使用它。

    float: function(a,b,c,d) {
        console.log("doing float", this.sourceString);
        return parseFloat(this.sourceString);
    },

现在我们的测试可以通过了:

test('4.8e10',4.8e10);
test('4.8e-10',4.8e-10);

结论

Ohm 是构建解析器的一个很棒的工具,因为它易于上手,并且你可以递增的增加规则。Ohm 也还有其他我今天没有写到的很棒的特点,比如调试观察仪和子类化。

到目前为止,我们已经使用 Ohm 来把字符串翻译成 JavaScript 数,并且 Ohm 经常用于把一种表示方式转化成另外一种。然而,Ohm 还有更多的用途。通过放入不同的语义功能集,你可以使用 Ohm 来真正处理和计算东西。一个单独的语法可以被许多不同的语义使用,这是 Ohm 的魔法之一。

在这个系列的下一篇文章中,我将向你们展示如何像真正的计算机一样计算像 (4.85 + 5 * (238 - 68)/2) 这样的数学表达式,不仅仅是解析数。

额外的挑战:你能够扩展语法来支持八进制数吗?这些以 8 为基底的数能够只用 0 到 7 这几个数字来表示,前面加上一个数字 0 或者字母 o。看看针对下面这些测试情况是够正确。下次我将给出答案。

test('0o77',7*8+7);
test('0o23',0o23);

via: https://www.pubnub.com/blog/2016-08-30-javascript-parser-ohm-makes-creating-a-programming-language-easy/

作者:Josh Marinacci 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出