在上一篇文章中,我们通过AST完成了微信小程序组件的多端编译,在这篇文章中,让我们更深入一点,通过AST完成一个javascript元循环求值器
结构
一个元循环求值器,完整的应该包含以下内容:
- tokenizer:对代码文本进行词法和语法分析,将代码分割成若干个token
- parser:根据token,生成AST树
- evaluate:根据AST树节点的type,执行对应的apply方法
- apply:根据环境,执行实际的求值计算
- scope:当前代码执行的环境
代码目录
根据结构看,我将代码目录大致拆分为以下几个文件
- parser
- eval
- scope
tokenizer和parser这两个过程不是本文的重点,我统一放在了parser中,交由 @babel/parser 来处理。
evaluate和apply这两个过程我统一放在了eval文件中处理,一会我们重点看下这部分。
scope则放入scope文件。
evaluate-apply
这其实是一个递归计算的过程。
首先,evaluate 接收两个参数,node 当前遍历的AST树节点和 scope 当前环境。然后,evaluate去根据 node 的 type 属性,判断该节点是什么类型。判断出类型后,执行 apply 去求值这个节点所代表的表达式。apply 中会再次递归的执行 evaluate 去计算当前节点的子节点。最终,执行完整颗AST树。
我们来看下具体代码吧
const evaluate = (node: t.Node, scope) => { const evalFunc = evaluateMap[node.type]; if (!evalFunc) { throw `${node.loc} ${node.type} 还未实现`; } return evalFunc(node, scope); }
以上就是evaluate具体做的事。
其中,evaluateMap 是目前实现的内容集合,我们来看下具体的代码
const evaluateMap: EvaluateMap = { File(node: t.File, scope) { evaluate(node.program, scope); }, Program(node: t.Program, scope) { for (const n of node.body) { evaluate(n, scope); } }, Identifier(node: t.Identifier, scope) { const $var = scope.$find(node.name); if (!$var) { throw `[Error] ${node.loc}, '${node.name}' 未定义`; } return $var.$get(); }, StringLiteral(node: t.StringLiteral, scope) { return node.value; }, NumericLiteral(node: t.NumericLiteral, scope) { return node.value; }, BooleanLiteral(node: t.BooleanLiteral, scope) { return node.value; }, NullLiteral(node: t.NullLiteral, scope) { return null; }, BlockStatement(block: t.BlockStatement, scope) { const blockScope = scope.shared "color: #ff0000">scope
我们再来看下 scope 该如何实现。
class Scope implements IScope { public readonly variables: EmptyObj = Object.create(null); constructor( private readonly scopeType: ScopeType, private parent: Scope = null, public readonly shared = false, ) { } }我们构造一个类来模拟 scope。可以看到,Scope 类包含了以下4个属性:
- variables:当前环境下存在的变量
- scopeType:当前环境的type
- parent:当前环境的父环境
- shared:有些时候不需要重复构造子环境,故用此标识
接下来我们看下该如何在环境中声明变量
首先构造一个类来模拟变量
class Variable implements IVariable { constructor( private kind: Kind, private value: any ){ } $get() { return this.value } $set(value: any) { if (this.kind === 'const') { return false } this.value = value; return true; } }
这个类中有两个属性和两个方法
- kind 用于标识该变量是通过 var、let 还是 const 声明
- value 表示该变量的值
- $get 和 $set 分别用于获取和设置该变量的值
有了 Variable 类之后,我们就可以编写 Scope 类中的声明变量的方法了。
let 和 const 的声明方式基本一样
$const(varName: string, value: any) { const variable = this.variables[varName]; if (!variable) { this.variables[varName] = new Variable('const', value); return true; } return false; } $let(varName: string, value: any) { const variable = this.variables[varName]; if (!variable) { this.variables[varName] = new Variable('let', value); return true; } return false; }
var 的声明方式稍微有一点差异,因为js中,除了在 function 中,用var 声明的变量是会被声明到父级作用域的(js的历史遗留坑)。我们看下代码
$var(varName: string, value: any) { let scope: Scope = this; while (!!scope.parent && scope.scopeType !== 'function') { scope = scope.parent; } const variable = scope.variables[varName]; if (!variable) { scope.variables[varName] = new Variable('var', value); } else { scope.variables[varName] = variable.$set(value); } return true }
除了声明,我们还需要一个寻找变量的方法,该方法会从当前环境开始,一直沿着作用域链,找到最外层的环境为止。因此,代码实现如下
$find(varName: string): null | IVariable { if (Reflect.has(this.variables, varName)) { return Reflect.get(this.variables, varName); } if (this.parent) { return this.parent.$find(varName); } return null; }
以上,一个基本的javascript元循环求值器就完成了
最后
大家可以在 codesandbox 在线体验一下。
完整的项目地址是:nvwajs,欢迎鞭策,欢迎star。
参考
《SICP》
微信小程序也要强行热更代码,鹅厂不服你来肛我呀
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com