【译】使用 Generator 构建一个内置于浏览器中的 JavaScript 虚拟机和调试器

发表于 5年以前  | 总阅读数:1444 次

原文:http://www.zcfy.cc/article/393

长文预警(tl;dr)

我用 JavaScript 创建了一个 JavaScript 虚拟机和调试器。你可以查看例子或是在 Github 下载源码。你可以继续阅读深入它的技术细节。

更新:使用 debug.js 我实现了一个 Bret Victor 的 Learnable Programming 的例子。

简介

动机

在过去的几年里,我致力于创建帮助用户在线学习编程的工具。我开发了 repl.it 并开源了它底层的技术为 udacity、codecademy、learnstreet 等在线网站提供了技术解决方案。直到最近,我加入 Codecademy 负责产品工程。经历过这一切,我最愿意看到的就是工具能够可视化地展现代码的执行过程并能够在浏览器中单步调试代码。要初步了解什么是完美的交互式在线学习环境,你可以体验 Bret Victor 的 Learnable Programming。(好东西总被墙啊,得翻墙 ╮(╯▽╰)╭----译者注)

除了对教育的好处外,这个工具如果将来成熟了,它将对代码植入、Web IDE 有帮助,而且还能为基于 JavaScript 实现其它虚拟机(有了可暂停的状态机让你不需要担心非阻塞环境)打下基础。

自从我得知了 ES6 Generators 提案,我的脑海里就一直有个不成熟的想法,却没有办法真正实现,直到 Ben Newman 发布了 Regenerator 真正把 generators 带进了浏览器。

目标

  • 能够运行 ES5 代码的 JS 虚拟机
  • 虚拟机能够暂停执行任意指令
  • 在虚拟机之上能够实现一个功能完整的 JS 调试器
  • 主要目标是浏览器,但是也能在 Node.js 开发中使用

Generators

如果你对 generators 熟悉,你可以跳过这一节或者阅读这篇文章来了解更全面的内容。

Generators 是 ES6 提案的一部分,随着浏览器的升级,它已经慢慢进入生产环境。Generators 引入了一种新的函数类型,它让我们能够进入和暂时跳出一个函数,并向函数发送和接收一些值。

下面的例子将阐明 generators 的基本运作方式:

function* genFn() {
      var x = yield 2;
      yield x;
      return "done";
    }

    var gen = genFn()
    console.log(gen.next());  // {value: 2, done: false}
    console.log(gen.next(1)); // {value: 1, done: false}
    console.log(gen.next());  // {value: "done", done: true}

注意 function 关键字末尾增加一个 * 表示这是一个 generator

概述

Generators 拥有独特的能力,能让一个函数暂停执行,然后在之后的一个时间点恢复执行。Generators 的这个能力给了我们创建一个可以在指令之间单步执行,并在任何一处暂停执行的虚拟机的基本构件。要达到这个目的,系统的每一个函数都必须要被转成一个 generator,并将每一条指令执行前的状态通过 yields 传给虚拟机。这可能类似于 Continuation Passing Style,尽管如此,两者的主要区别在于 CPS 的调用栈被保存在词法作用域上,然而用我的这个方法我们需要完全控制整个调用栈。我不是一个编译器专家或者一个 PLT (Programming language theory----译者注)专家,因此我不是非常确定我的这个方法是否有名字或者是否之前有人尝试过,如果你知道,请你告诉我。

我们希望 JavaScript 宿主环境尽可能多承担运行代码的职责。除了 generator 与函数转换之外我们还需要准备一些别的东西,我会先将它们列出来,稍后一一讨论。

  • 控制调用栈
  • 处理错误和将错误冒泡
  • 控制系统的定时器(setTimeout, setInterval 等等)
  • 解决那些需要函数作为参数(得是 callback 函数,不能是 generator)的原生 API
  • 实现一个调试器模块

Code Transformation

转换代码

为了控制执行流,我们需要在每一条指令执行之后 yield 回虚拟机。要做到这个,我们在程序的每一条指令之前插入一个 yield 表达式。这里我将一条 JavaScript 语句定义成一条指令(或者说一个执行步骤)。

例如:

var foo = 1;
    if (bar === foo) {
      foo = 2;
    }

经过转换后:

function* __top() {
      yield {step};
      var foo = 1;
      yield {step};
      if (bar === foo) {
        yield {step};
        foo = 2;
      }
    }

除了基本的指令转换之外,我们需要在程序中添加关于每一个函数的信息,我们将要实现的调试器要用到那些信息。我们把这些信息叫做堆栈帧,它包含了当前函数的如下数据:

  • 函数名
  • 文件名
  • 作用域:一个变量名数组,数组中的变量出现在函数中。
  • eval 函数:调试器通过它访问函数闭包来做一些事情,例如监视表达式的值并在当前作用域执行一些代码。

最后,函数调用处理起来比普通的指令棘手一些,因为我们需要捕获调用栈,并且还要很好地支持原生函数和库函数调用。编译时,在函数调用处,我们不知道是否函数调用引用了一个 generator 函数(一个经过我们转换的,在我们生态系统之内的函数)或者一个函数对象。如果是前者,我们需要添加它到我们的调用栈并进入函数体执行指令,如果是后者,我们只要执行它得到一个值然后返回。我们最终解决了这个难题,我们将所有的函数调用包装在一个 thunk 中然后将它 yield 回虚拟机来使得前面的问题可以在运行时决定(运行时我们能获得更多的信息)。

"Thunk" 这个词描述了 JavaScript 程序员经常做的一件事----创建一个闭包,让一段代码延迟执行。例如:

`foo();`

转换为:

yield __thunk(function *thunk() {
      return foo();
    }, this, arguments);

更复杂的调用表达式也没问题:

`for (var i = foo(), b = bar(); i < 50; i++);`

转换为:

for (var i = yield __thunk(function* thunk() {
      return foo();
    }, this, arguments), b = yield __thunk(function* thunk() {
      return bar();
    }, this, arguments); i < 50; i++);

thisarguments 被传递给 thunk 因此我们可以在 thunk 被执行时创建正确的作用域。

虚拟机

单步执行和调用栈

我们的虚拟机的主要职责是管理调用栈,压入、推出和调用其中的函数。它开始于一个停顿(或者说空闲状态)直到我们执行一个已经被转换为一个顶层的 generator 的代码字符串。然后,我们可以调用 虚拟机的 step 来执行 generator 的 next() 方法,让代码中的下一条指令执行。如果这条指令返回一个 thunk,我们执行它,如果它返回一个 generator,我们将它 push 进我们的调用栈,以备后续的执行步骤调用它。等到我们当前的 generator 执行完毕,我们获得最后的值,将它传给调用栈中的下一个 generator。传递值也是通过 generator 的 .next 方法,它接受一个参数,将这个参数传给 generator 函数。

错误处理

当执行一个指令时,有可能它会抛出一个错误。我们处理它的方式是我们 try/catch 每一条指令执行,并且如果我们获得一个错误,我们将它沿着调用栈往上传,直到其中一个调用函数有自己的 try/catch 语句。

Runner.prototype.$propError = function (e) {
      while (this.stack.length) {
        this.gen = this.stack.pop();
        try {
          this.gen.throw(e);
          return;
        } catch (e2) {
          e = e2;
        }
      }
      throw e;
    };

定时器

我们的其中一个目标是能够在任意时间点上暂停我们的程序执行,我们想要它暂停多久就暂停多久。由于这个原因,我们遇到一个问题,我们无法依赖宿主 JavaScript 环境来控制定时器。比如我们有一个 setInterval 每 1 秒钟执行一次,然而我们决定暂停程序 10 秒钟,当我们恢复程序运行时,我们不能接受 10 下连续的定时器触发。虚拟机的时钟应该只在以下情况下有效:

  1. 我们在执行指令代码。
  2. 我们处于空闲状态(调用栈空了并且虚拟机处在停顿中)。

我们使用一个优先队列来保存我们的定时器,然后用一个 tick 方法检查当前时间点是否有任何 timer 要被触发。我们依赖于宿主的 setImmedate 或者 setTimeout(tick, 0) 来提供我们需要的 tick 函数。

当一个定时器被触发,虚拟机简单发起一个 timer 事件,程序将响应这个事件进入 timer 执行(这将创建一个新的调用栈)。这个模式更像 JavaScript 的事件循环而非原生定时器。

原生 API

我们不能期望我们的代码用到的每个原生 API 都能处理我们转换过来的 generators,因此虚拟机提供了一个方法将回调包装起来,并且将它们 yield 回虚拟机以备后续执行。然而,当参数有 callback 但实际上是同步 API 的时候,比如 Array.forEach 将连续调用回调直到迭代器完成,这种情况下一个问题会产生,因为我们是期望能够暂停任何指令执行,但我们没法让 Array.forEach 这种系统原生 API 能做到同样的事情。幸运地是,这个问题不难解决,我们所要做的只是使用流行的 es5-shim 库,用我们的转换处理它,用它代替原生 API。

事件

与原生 API 的问题非常类似,事件监听是通过函数,但是这是一个非常简单的问题,因为这是异步 API,我们所需要做的仅仅是将事件监听函数用一个函数包装器包装一下,让它可以在被调用时激活我们的虚拟机。

调试器

创建了虚拟机之后,实现调试器非常有趣而且很简单。我遇到的唯一一个问题是处理调用栈中来自 thunk 的多余信息,因为我们将 thunk 当做普通函数来调用,这样让虚拟机变得简单。

特性:

  • 断点
  • debugger 语句
  • Step in、step out 和 step over 任意语句。
  • 得到作用域下的变量和值
  • 得到调用栈
  • 在作用域中执行命令:不论何时你在一个断点里你可以在当前作用域里执行代码。

在这里获得项目源码。

目前状态

这个项目还在早期开发中。我才为它工作了大约两个星期。在正确性方面,我确信虚拟机可以运行绝大多数 ES5 特性。在我写这篇文章的时候,我想起一个问题,现在这个情况下,在虚拟机里使用 getters 和 setters 肯定会有问题

这个虚拟机现在还很慢,尤其是在转换代码时,但是我们在提升速度方面马上要取得一些进展了。

我也意识到 generator 转换只是一个中间步骤,目的是让 regenerator 将代码变成能够自由地 step in/step out 的函数。因此,我们可以抛弃那个步骤,直接将代码转换成状态机。(也就是《用 JavaScript 实现单步调试》这篇文章的作者的做法,这样明显性能要好很多----译者注)。

英文原文:http://amasad.me/2014/01/06/building-an-in-browser-javascript-vm-and-debugger-using-generators/

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
为Electron程序添加运行时日志 5年以前  |  20340次阅读
Node.js下通过配置host访问URL 5年以前  |  5892次阅读
用 esbuild 让你的构建压缩性能翻倍 4年以前  |  5785次阅读
 目录