生成函数中的JS等待/暂停

4

我在做什么

我正在使用Blockly构建一个海龟图形应用程序。用户可以从块中构建代码,然后Blockly引擎生成JS代码,绘制到画布上。

我的问题是什么

Blockly引擎生成JS代码,但将其作为字符串返回,我必须使用eval()来绘制到画布上。

我可以更改块的代码以生成不同的输出,但是尽可能保持简单很重要,因为用户可以读取块输入后面的实际代码。因此,我不想弄糊它。

我想做什么

我对原子操作(goturn等)具有完全控制权,因此我想在函数开头插入一小段代码,延迟执行其余函数体的代码。类似于:

function go(dir, dist) {
  // wait here a little

  // do the drawing
}

我认为应该是一些同步的东西,可以保持执行流程中的延迟。我尝试过使用setTimeout(异步,失败),promise(失败),循环中的时间戳检查(失败)。

在JS中是否可能实现?


1
你为什么想要这样的延迟?比如说展示一些动画吗? - Tamas Hegedus
等待是什么意思?如果你的意思是阻塞JS执行引擎,那么循环是唯一的方法。 - zhang
你无法延迟同步执行。你必须生成异步代码,但据我所见,目前还没有适用于Blocky的这种代码生成器。这样做意义不大,因为异步变体更难阅读。但是有一个JS解释器,你可以使用它来异步(且安全地)逐行运行代码。 - Tamas Hegedus
1
如果你正在使用支持async/await的环境,你可以编写看似同步的代码,但最终必须是异步的。 - zzzzBov
@TamasHegedus 这是一款教育应用程序。逐步了解形状的创建过程非常有益。 - Nekomajin42
3个回答

3

您不能同步等待代码执行。唯一的结果就是冻结浏览器窗口。

您需要使用JS解释器,而不是使用eval函数。这样您可以暂停代码执行,播放动画,突出显示正在执行的块等。该教程有许多示例可帮助您入门。以下是一个基于JS解释器示例的可工作代码:

var workspace = Blockly.inject("editor-div", {
  toolbox: document.getElementById('toolbox')
});

Blockly.JavaScript.STATEMENT_PREFIX = 'highlightBlock(%1);\n';
Blockly.JavaScript.addReservedWords('highlightBlock');

Blockly.JavaScript['text_print'] = function(block) {
  var argument0 = Blockly.JavaScript.valueToCode(
    block, 'TEXT',
    Blockly.JavaScript.ORDER_FUNCTION_CALL
  ) || '\'\'';
  return "print(" + argument0 + ');\n';
};

function run() {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  var running = false;

  workspace.traceOn(true);
  workspace.highlightBlock(null);

  var lastBlockToHighlight = null;
  var myInterpreter = new Interpreter(code, (interpreter, scope) => {
    interpreter.setProperty(
      scope, 'highlightBlock',
      interpreter.createNativeFunction(id => {
        id = id ? id.toString() : '';
        running = false;
        workspace.highlightBlock(lastBlockToHighlight);
        lastBlockToHighlight = id;
      })
    );
    interpreter.setProperty(
      scope, 'print',
      interpreter.createNativeFunction(val => {
        val = val ? val.toString() : '';
        console.log(val);
      })
    );
  });

  var intervalId = setInterval(() => {
    running = true;
    while (running) {
      if (!myInterpreter.step()) {
        workspace.highlightBlock(lastBlockToHighlight);
        clearInterval(intervalId);
        return;
      }
    }
  }, 500);
}
#editor-div {
  width: 500px;
  height: 150px;
}
<script src="https://rawgit.com/google/blockly/master/blockly_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/blocks_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/javascript_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/msg/js/en.js"></script>
<script src="https://rawgit.com/NeilFraser/JS-Interpreter/master/acorn_interpreter.js"></script>

<xml id="toolbox" style="display: none">
  <block type="text"></block>
  <block type="text_print"></block>
  <block type="controls_repeat_ext"></block>
 <block type="math_number"></block>
</xml>

<div>
  <button id="run-code" onclick="run()">run</button>
</div>
<div id="editor-div"></div>

编辑

添加了变量running来控制解释器。现在,它会一直执行,直到running变量被设置为false,所以highlightBlock函数中的running = false语句基本上可以作为断点。

编辑

引入了lastBlockToHighlight变量来延迟高亮显示,因此最新的运行语句会被突出显示,而不是下一个语句。不幸的是,JavaScript代码生成器没有类似于STATEMENT_PREFIXSTATEMENT_SUFFIX配置。


解释器在评估表达式时执行许多步骤。在其中一个示例中,他们使用全局变量来在特定表达式后中断,让我们看看我能否复制相同的行为。 - Tamas Hegedus
1
@Nekomajin42:添加了一些改进。 - Tamas Hegedus
好的,终于可以了。我还有一个问题。块高亮和代码执行不同步。当高亮显示块#2时,会执行代码#1,依此类推。你的代码片段似乎也有这个问题。 - Nekomajin42
@Nekomajin42 我是故意这么做的。对我来说,突出下一个要执行的语句似乎很合理。我将编辑片段。 - Tamas Hegedus
1
@Nekomajin42 给你。 - Tamas Hegedus
显示剩余3条评论

2
最近我发布了一个库,可以让你与Blockly进行异步交互。我设计了这个库用于类似游戏的应用。实际上,在文档中,你可以找到一个游戏演示,它是迷宫游戏的翻版。该库名为blockly-gamepad ,希望这正是你想要的。


以下是演示的gif动画。

Demo


工作原理

这是一种与普通使用Blockly不同且简化的方法

首先,你需要定义 (请参阅文档中的定义方法)。你不需要定义任何代码生成器,所有关于代码生成的内容都由库执行。

enter image description here


每个块生成一个请求

// the request
{ method: 'TURN', args: ['RIGHT'] }

当一个块被执行时,相应的请求会传递到您的游戏中。最初的回答。
class Game{
    manageRequests(request){
        // requests are passed here
        if(request.method == 'TURN')
            // animate your sprite
            turn(request.args)
    }
}

你可以使用promises来管理异步动画,就像你的情况一样,这是最初的回答。
class Game{
    async manageRequests(request){
        if(request.method == 'TURN')
            await turn(request.args)
    }
}


游戏块与您的游戏之间的链接由游戏手柄管理。


let gamepad = new Blockly.Gamepad(),
    game = new Game()

// requests will be passed here
gamepad.setGame(game, game.manageRequest)

游戏手柄提供了一些方法来管理块的执行,从而生成请求。"最初的回答"。
// load the code from the blocks in the workspace
gamepad.load()
// reset the code loaded previously
gamepad.reset()

// the blocks are executed one after the other
gamepad.play() 
// play in reverse
gamepad.play(true)
// the blocks execution is paused
gamepad.pause()
// toggle play
gamepad.togglePlay()

// load the next request 
gamepad.forward()
// load the prior request
gamepad.backward()

// use a block as a breakpoint and play until it is reached
gamepad.debug(id)

你可以在这里阅读完整的文档。 编辑:我更新了库的名称,现在它被称为blockly-gamepad

嗨,Paolo。请问你能解释一下你的库是如何修复 OP 所提出的问题的吗? - Mike Poole
你可以在这里找到它的工作原理解释:https://paol-imi.github.io/gamepad.js/#/theidea。@MikePoole - Paolo Longo
嗨,Paolo。我的评论重点是你应该在SO中解释这个工作原理,而不是链接到另一个可能会失效的网站。 - Mike Poole
嗨,Mike,链接重定向到不应该死亡的github文档。现在我修改答案以使一切更清晰,感谢您的建议。 - Paolo Longo

1

如果我理解正确!

您可以构建一个新类来处理执行go(dir, dist)函数,并覆盖go函数以在执行器中创建新的go

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            originalGoFunction(currentExec.dir, currentExec.dist);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

// Override go function
var originalGoFunction = go;
var go = function (dir, dist, delay){
    goExec.push({"dir":dir, "dist":dist, "delay":delay});
}

编辑1:

现在您需要调用callWithDelay并传入您的函数和参数,执行程序将通过将参数应用于指定的函数来处理此调用。

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            currentExec.funcNam.apply(currentExec.funcNam, currentExec.arrayParams);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

var callWithDelay = function (func, arrayParams, delay){
    goExec.push({"func": func, "arrayParams":arrayParams, "delay":delay});
}

但是我有几个不同名称和参数列表的函数。 - Nekomajin42
@Nekomajin42 请看“编辑1”。 - David Antoon
而且它们的顺序是随机的,因为用户构建的代码是由Blockly生成的。 - Nekomajin42
那么你应该使用回调函数,你提到过“我尝试过使用setTimeout(异步,失败),Promise(失败),在循环中进行时间戳检查(失败)”,你是如何尝试实现它的? - David Antoon
1
我会坚持@Tamas的解决方案,但我会点赞你,因为它可能对其他人有帮助。谢谢! - Nekomajin42

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接