在我的上一篇文章中,我介绍了 Unwinder,它实现了 JavaScript 的“延续性(continuations)”。延续性和单步调试有什么关系呢?Unwinder 使用延续性实现了一个调试器,它可以保持状态,在任何时候暂停代码的执行。
这篇文章本来可以用“实现 JavaScript 的延续性”来作为标题,但是比起了解延续性,更多人知道的是“单步调试工具”。此外,实现一个单步调试工具,是延续性的一个非常 cool 的用途。
如果你还没有读过前一篇文章 (这篇文章深入解释了什么是延续性,并使大量用了单步调试来详细描述它),这里有一个活生生的单步调试工具:
你可以点击上面的图片链接,跳转到一个线上demo,点击任何一行代码设置一个断点
它很酷,对吗?好吧,这的确不同寻常,它不是什么从视觉上看起来有趣的东西,但是,它是一个完全用 JavaScript 实现的调试器,完全不依赖原生的 JavaScript 引擎。我的前一篇文章介绍了更多关于使用 Unwinder 的内容。
具体实现与一个复杂的变换有关,这是在一篇论文“Exceptional Continuations in JavaScript”中被提出的理论。这篇论文思路清晰且通俗易懂,如果你对它感兴趣,你可以阅读它。这项技术的亮点是性能:我们依然使用原生函数的作用域、变量和其他的部分,这样 JIT 依然可以对它们如平时一样进行深度优化。同时,代码在执行过程中被检测,从而使我们可以保存调用堆栈信息。
无论是延续性还是单步调试都需要在任意时刻可以暂停执行代码。这意味着我们需要保存整个调用栈,并且能够在需要继续运行的时候恢复它。让我们来看一下论文中描述的实现上面的需求的具体技术方案。由于论文写得非常清晰,我能够很愉快地将基本步骤写下来。
状态机
第一步,我们需要将代码编译成我们可以控制的某种结构,即将代码变成状态机,这是一种常见的变换。Facebook 的 regenerator 编译 generator、Clojure 编译 Go 语言模块,以及其他的一些编译工具,也都用了状态机。Unwinder 实际上是从 fork regenerator 的项目代码开始的。
让我们看以下代码:
1 2 3 4 5 |
function foo() { var x = 5; var y = 6; return x + y; } |
将函数 foo
变换成状态机,看起来像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function foo() { var $__next = 0, x, y; while(1) { switch($__next) { case 0: x = 5; $__next = 1; break; case 1: y = 6; $__next = 2; break; case 2: return x + y; } } } |
$__next
是关键点:它精确控制着当前执行哪一个步骤。注意到我们将 x
和 y
的声明放在最前面,因为我们需要重新实现变量作用域。我们将各个代码块编译进了case
语句中,而函数声明被提到前面,我们还需要维持作用域并屏蔽用 var
、 let
或者 const
声明的同名变量。
你应该已经了解这不是一个简单的小把戏。我们才刚开始,生成的代码就已经是比原始代码复杂很多了,但是,它在小规模的程序中能很好地工作,它并不是为了真正线上产品而设计的,它只为了演示小 demo 和实现一些小功能。
重新实现原生控制结构
让我们看一些更复杂的代码,在下面的代码里我们有 while
循环,而且有同名的变量需要屏蔽:
1 2 3 4 5 |
function foo(x) { while(Math.random() .3) { let x = 5; console.log(x); } } |
假如我们要暂停执行在 while 循环内部的代码该怎么做?我们当然不能使用原生的 while
语句,我们必须在我们的状态机中重新实现我们的循环。一个简版的编译后的代码大概看起来如下:
这里还需注意的是,我们通过将局部的 x
变量改名为 x$0
来屏蔽函数参数里的同名变量
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 28 29 30 31 |
function foo(x) { var $__next = 0, $__t1, x$0; while(1) { switch($__next) { case 0: $__t1 = Math.random(); $__next = 1; break; case 1: if(!($__t1 .3)) { $__next = 5; } $__next = 2; break; case 2: x$0 = 5; $__next = 3; break; case 3: console.log(x$0) $__next = 4; break; case 4:/div> break; case 4:BA%8C%E6%80%A7">延续性(continuations)”。延续性和单步调试有什么关系呢?Unwinder 使用延续性实现了一个调试器,它可以保持状态,在任何时候暂停代码的执行。
这篇文章本来可以用“实现 JavaScript 的延续性”来作为标题,但是比起了解延续性,更多人知道的是“单步调试工具”。此外,实现一个单步调试工具,是延续性的一个非常 cool 的用途。 如果你还没有读过前一篇文章 (这篇文章深入解释了什么是延续性,并使大量用了单步调试来详细描述它),这里有一个活生生的单步调试工具: 你可以点击上面的图片链接,跳转到一个线上demo,点击任何一行代码设置一个断点
它很酷,对吗?好吧,这的确不同寻常,它不是什么从视觉上看起来有趣的东西,但是,它是一个完全用 JavaScript 实现的调试器,完全不依赖原生的 JavaScript 引擎。我的前一篇文章介绍了更多关于使用 Unwinder 的内容。 具体实现与一个复杂的变换有关,这是在一篇论文“Exceptional Continuations in JavaScript”中被提出的理论。这篇论文思路清晰且通俗易懂,如果你对它感兴趣,你可以阅读它。这项技术的亮点是性能:我们依然使用原生函数的作用域、变量和其他的部分,这样 JIT 依然可以对它们如平时一样进行深度优化。同时,代码在执行过程中被检测,从而使我们可以保存调用堆栈信息。 无论是延续性还是单步调试都需要在任意时刻可以暂停执行代码。这意味着我们需要保存整个调用栈,并且能够在需要继续运行的时候恢复它。让我们来看一下论文中描述的实现上面的需求的具体技术方案。由于论文写得非常清晰,我能够很愉快地将基本步骤写下来。 状态机第一步,我们需要将代码编译成我们可以控制的某种结构,即将代码变成状态机,这是一种常见的变换。Facebook 的 regenerator 编译 generator、Clojure 编译 Go 语言模块,以及其他的一些编译工具,也都用了状态机。Unwinder 实际上是从 fork regenerator 的项目代码开始的。 让我们看以下代码:
将函数
你应该已经了解这不是一个简单的小把戏。我们才刚开始,生成的代码就已经是比原始代码复杂很多了,但是,它在小规模的程序中能很好地工作,它并不是为了真正线上产品而设计的,它只为了演示小 demo 和实现一些小功能。 重新实现原生控制结构让我们看一些更复杂的代码,在下面的代码里我们有
假如我们要暂停执行在 while 循环内部的代码该怎么做?我们当然不能使用原生的 这里还需注意的是,我们通过将局部的
|