只有在大 for 循环结束后才会调用回调

Callback is being called only after big for loop ends

我正在通过 websockets (paho-mqtt) 在浏览器中接收数据,但问题是接收回调仅在另一个任务结束时被触发(大 for 循环)并且它被所有堆叠数据触发,我'我不会丢失数据,只是会延迟。即使有一个循环 运行 也不应该触发回调吗?这里发生了什么?否则,我如何实现这一点,在循环中不断接收?

我想说的相当于以下内容:

如果我在 chrome

中这样做
setTimeout(() => {
  console.log('hello!');
}, 10);
for (var i = 0; i < 50000; i++) {
  console.log('for array');
}

我明白了

50000 VM15292:5 for array
VM15292:2 hello!

我不应该得到这样的东西吗?

1000 VM15292:5 for array
VM15292:2 hello!
49000 VM15292:5 for array

Javascript 引擎趋向于单线程。

因此,如果您处于 运行 不屈服的紧密循环中(例如,执行一些 io),那么在循环完成之前,回调永远不会有机会 运行

当您在浏览器中运行 JavaScript 编写代码时(除非使用Web Workers 或其他特殊技术),它是在单线程上执行的。这听起来可能不太重要,但确实如此。

您的代码包含一个 for 循环(同步)和一个对 setTimeout 的调用(异步)。由于一次只能 JavaScript 中的一个 运行ning,因此您的 for 循环将永远不会被 setTimeout.

打断

事实上,如果您的 for 循环包含需要超过 10 毫秒才能完成的极其密集的操作,您的 setTimeout 回调实际上可能会 延迟 超过该标记,因为浏览器在继续 运行 事件循环之前总是等待当前执行的代码完成。

setTimeout(() => {
  console.log('hello!');
}, 10);
for (var i = 0; i < /* 50000 */ 5; i++) {
  console.log('for array');
}

其他人已经很好地诊断了问题,浏览器的单线程特性。我将提供一个可能的解决方案:发电机。

这是演示问题的代码笔: http://codepen.io/anon/pen/zZwXem?editors=1111

window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 60000;

function log(message) {
  const output = document.getElementById('output');
  output.value = output.value + '\n' + message;
}

function asyncTask() {
  log('Simulated websocket message')
}


function doWork() {
  const timer = setInterval(1000, asyncTask);
  let total = 0;
  for (let i = 1; i < 100000000; i++) {
    const foo = Math.log(i) * Math.sin(i);
    total += foo;
  }
  log('The total is: '+ total);
  clearInterval(timer);
}

当通过单击 'Do Work' 按钮调用 doWork() 时,asyncTask 永远不会 运行s,并且 UI 会锁定。糟糕的用户体验。

以下示例使用生成器来 运行 长 运行ning 任务。

http://codepen.io/anon/pen/jBmoPZ?editors=1111

//Basically disable codepen infinite loop detection, which is faulty for generators
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 120000;

let workTimer;

function log(message) {
  const output = document.getElementById('output');
  output.value = output.value + '\n' + message;
}

function asyncTask() {
  log('Simulated websocket message')
}

let workGenerator = null;
function runWork() {
  if (workGenerator === null) {
    workGenerator = doWork();
  }
  const work = workGenerator.next();
  if (work.done) {
      log('The total is: '+ work.value);
      workerGenerator = null;
  } else {
    workTimer = setTimeout(runWork,0);
  }
}

function* doWork() {
  const timer = setInterval(asyncTask,1000);
  let total = 0;
  for (let i = 1; i < 100000000; i++) {
    if (i % 100000 === 0) {
      yield;
    }
    if (i % 1000000 == 0) {
      log((i / 100000000 * 100).toFixed(1) + '% complete');
    }
    const foo = Math.log(i) * Math.sin(i);
    total += foo;
  }
  clearInterval(timer);
  return total;
}

这里我们在生成器中工作,并创建一个生成器 运行ner 以从 UI 中的 'Do Work' 按钮调用。这个运行s在最新版本Chrome上,我不能代表其他浏览器。通常,您会使用 babel 之类的工具将生成器编译为 ES5 语法以用于生产构建。

生成器每 10000 行计算产生一次,并且每 100000 行发出一次状态更新。生成器 运行ner 'runWork' 创建生成器的一个实例并重复调用 next()。然后生成器 运行s 直到它命中下一个 'yield' 或 return 语句。在生成器 yield 之后,生成器 运行ner 然后通过调用 0 毫秒的 setTimeout 并将其自身用作处理函数来放弃 UI 线程。这通常意味着它将在每个动画帧(理想情况下)被调用一次。这一直持续到生成器 returns 完成标志,此时生成器 运行ner 可以获得 returned 值并清理。

这里是 HTML 示例,以防您需要重新创建代码笔:

<input type='button' value='Do Work' onclick=doWork() />
<textarea id='output' style='width:200px;height:200px'></textarea>