如何在JavaScript中实现eval()逐语句暂停或使用可暂停虚拟机?
解决方案:逐行可控执行用户代码
核心思路:AST转换 + 生成器函数
直接修改用户代码插入异步暂停逻辑容易触发兼容问题,更可靠的方式是通过解析用户代码为抽象语法树(AST),将代码转换为可逐步控制的生成器函数,无需手动修改用户代码文本,就能实现每执行完一个语句后暂停,等待游戏视图同步完成后再继续执行。
具体实现步骤
- 解析用户代码为AST
使用@babel/parser或acorn将用户代码解析为抽象语法树,精准识别语句结构,规避字符串替换的风险:
import { parse } from '@babel/parser'; const userCode = ` function controlPlayer() { move('up'); move('right'); attack(); } controlPlayer(); `; const ast = parse(userCode, { sourceType: 'script' });
- 转换AST为带暂停点的生成器函数
用@babel/traverse遍历AST,将每个可执行语句包裹成yield表达式,再通过@babel/generator生成转换后的代码并包装为生成器函数:
import traverse from '@babel/traverse'; import generate from '@babel/generator'; import * as t from '@babel/types'; traverse(ast, { ExpressionStatement(path) { path.replaceWith(t.yieldExpression(path.node.expression)); }, BlockStatement(path) { path.node.body = path.node.body.map(stmt => { if (t.isExpressionStatement(stmt)) { return t.yieldExpression(stmt.expression); } return stmt; }); } }); const transformedCode = ` function* runUserCode() { ${generate(ast).code} } `;
- 控制生成器的执行节奏
创建生成器迭代器,通过异步函数控制执行流程,每执行到yield点就等待游戏视图同步(比如角色移动动画完成),再触发下一步:
// 定义用户代码依赖的游戏函数 const move = (direction) => { // 触发角色移动逻辑 console.log(`Moving ${direction}`); // 返回Promise等待动画完成 return new Promise(resolve => setTimeout(resolve, 1000)); }; // 执行转换后的代码得到生成器函数 eval(transformedCode); const iterator = runUserCode(); // 逐步执行逻辑 async function executeStepByStep() { let result = iterator.next(); while (!result.done) { if (result.value instanceof Promise) { await result.value; } result = iterator.next(); } } // 启动执行 executeStepByStep();
替代方案:沙箱虚拟机隔离执行
如果需要完全隔离用户代码与游戏主环境,可使用vm2这类沙箱库,在独立虚拟机中运行代码,同时自定义上下文配合生成器实现逐步暂停:
const { VM } = require('vm2'); const vm = new VM({ sandbox: { move: (direction) => { return new Promise(resolve => setTimeout(() => { console.log(`VM: Moving ${direction}`); resolve(); }, 1000)); } } }); // 将用户代码转换为生成器后传入沙箱执行 const userGenerator = vm.run(` function* userCode() { yield move('up'); yield move('right'); } userCode(); `); async function runInVM() { let iter = userGenerator; let result = iter.next(); while (!result.done) { await result.value; result = iter.next(); } } runInVM();
关键注意事项
- 安全性:必须用沙箱隔离用户代码,防止恶意操作游戏核心逻辑,
vm2能提供较好的环境隔离能力。 - AST转换完整性:需处理条件、循环等复杂语句,确保循环内部的每一次迭代也能正确暂停,可能需要优化AST遍历逻辑。
- 错误处理:捕获生成器执行中的异常,避免崩溃游戏主线程。
内容的提问来源于stack exchange,提问作者inane
相关产品推荐
相关产品推荐

