如何制作非阻塞的javascript代码?

Posted

技术标签:

【中文标题】如何制作非阻塞的javascript代码?【英文标题】:How to make non-blocking javascript code? 【发布时间】:2014-12-24 07:23:54 【问题描述】:

如何进行简单的非阻塞 javascript 函数调用?例如:

  //begin the program
  console.log('begin');
  nonBlockingIncrement(10000000);
  console.log('do more stuff'); 

  //define the slow function; this would normally be a server call
  function nonBlockingIncrement(n)
    var i=0;
    while(i<n)
      i++;
    
    console.log('0 incremented to '+i);
  

输出

"beginPage" 
"0 incremented to 10000000"
"do more stuff"

如何形成这个简单的循环来异步执行并通过回调函数输出结果?这个想法是不阻止“做更多的事情”:

"beginPage" 
"do more stuff"
"0 incremented to 10000000"

我尝试过遵循有关回调和延续的教程,但它们似乎都依赖于外部库或函数。他们都没有在真空中回答这个问题:如何将 Javascript 代码编写为非阻塞的!?


在问之前我已经非常努力地寻找这个答案;请不要以为我没有看。我发现的所有内容都是 Node.js 特定的([1]、[2]、[3]、[4]、[5])或其他特定于其他函数或库的内容([6]、[7]、[8]、 [9]、[10]、[11]),尤其是 JQuery 和 setTimeout()。请帮助我使用 Javascript 编写非阻塞代码,而不是像 JQuery 和 Node 这样的 Javascript 编写的工具。 请在将其标记为重复之前重新阅读该问题。

【问题讨论】:

毫不费力。您实际上必须告诉线程休眠一段时间才能阻塞线程。为避免睡眠,请使用带有回调的计时器。 sitepoint.com/settimeout-example 没有办法做到这一点。 Javascript 不是多线程的,只能对任务进行排队。您可以稍后执行长时间运行的任务,但不能与其他任务同时执行。 @AndrewHoffman 我不确定你是否理解。你不能让 JS 进入睡眠状态,但你可以让它忙到 UI 循环不能服务任何事件。 你可以用警报之类的东西来阻止线程,我希望每个浏览器都会禁用它。糟糕的程序员冻结了我的浏览器。 -_-' 在mozilla developer network 中搜索fork()exec()pthread(),你会发现是空的。为什么?因为对子进程和线程的支持不是浏览器 javascript 的标准功能。 Web workers 是一项实验性功能,旨在创建可以通信但不共享范围的其他进程。不支持按照您的建议同时运行 CPU 代码。实际上,引用的所有“异步”JS 代码都是关于 I/O 事件的。在 I/O 上:blah() 【参考方案1】:

为了让你的循环不阻塞,你必须把它分成几个部分,让 JS 事件处理循环在继续下一个部分之前消耗用户事件。

实现这一点的最简单方法是做一定量的工作,然后使用setTimeout(..., 0) 将下一块工作排队。至关重要的是,这种排队允许 JS 事件循环在继续下一项工作之前处理同时排队的任何事件:

function yieldingLoop(count, chunksize, callback, finished) 
    var i = 0;
    (function chunk() 
        var end = Math.min(i + chunksize, count);
        for ( ; i < end; ++i) 
            callback.call(null, i);
        
        if (i < count) 
            setTimeout(chunk, 0);
         else 
            finished.call(null);
        
    )();

有用法:

yieldingLoop(1000000, 1000, function(i) 
    // use i here
, function() 
    // loop done here
);

参见http://jsfiddle.net/alnitak/x3bwjjo6/ 的演示,其中callback 函数只是将一个变量设置为当前迭代计数,一个单独的基于setTimeout 的循环轮询该变量的当前值并使用其值更新页面。

【讨论】:

感谢您将这样的工作纳入您的答案,但是(就像您在评论中提到的那样)for 循环只是一个模拟需要很长时间的虚拟函数。除非我误解了某些东西,否则这段代码只对那种特殊情况有效。 @user1717828 哦,好吧。简短的回答是,你不能像你一样写你的三行并期望它工作 - 你必须调用你的长时间运行的任务(异步),然后安排在完成时调用另一个函数,就像我做的那样在我的yieldingLoop 示例中使用finished 回调。原程序流程将不间断地进行。 执行过程中能不能在屏幕上画点东西?【参考方案2】:

带有回调的 SetTimeout 是要走的路。不过,请了解您的函数范围与 C# 或其他多线程环境中的不同。

Javascript 不会等待函数的回调完成。

如果你说:

function doThisThing(theseArgs) 
    setTimeout(function (theseArgs)  doThatOtherThing(theseArgs); , 1000);
    alert('hello world');

您的警报将在您传递的函数之前触发。

不同之处在于警报阻止了线程,但您的回调没有。

【讨论】:

比起@Alnitak,我更喜欢这个答案的清晰性。但是,正如@Alnitak 指出的那样,值得注意的是,也可以使用setTimeout(..., 0) 来避免不必要的等待时间。它仍然是非阻塞的! 我同意,setTimeout(..., 0) 有助于在事件调用堆栈空闲时避免不必要的延迟。【参考方案3】:

据我所知,一般有两种方法可以做到这一点。一种是使用setTimeout(或requestAnimationFrame,如果您在支持环境中这样做)。 @Alnitak 在另一个答案中展示了如何做到这一点。另一种方法是使用 web worker 在一个单独的线程中完成你的阻塞逻辑,这样主 UI 线程就不会被阻塞。

使用requestAnimationFramesetTimeout

//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) 
  if (done) 
    console.log('0 incremented to ' + currentI);
  
);
console.log('do more stuff'); 

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback)
  var i = 0;
  
  function loop () 
    if (i < n) 
      i++;
      callback(i, false);
      (window.requestAnimationFrame || window.setTimeout)(loop);
    
    else 
      callback(i, true);
    
  
  
  loop();

使用网络工作者:

/***** Your worker.js *****/
this.addEventListener('message', function (e) 
  var i = 0;

  while (i < e.data.target) 
    i++;
  

  this.postMessage(
    done: true,
    currentI: i,
    caller: e.data.caller
  );
);



/***** Your main program *****/
//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) 
  if (done) 
    console.log('0 incremented to ' + currentI);
  
);
console.log('do more stuff'); 

// Create web worker and callback register
var worker = new Worker('./worker.js'),
    callbacks = ;

worker.addEventListener('message', function (e) 
  callbacks[e.data.caller](e.data.currentI, e.data.done);
);

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback)
  const caller = 'nonBlockingIncrement';
  
  callbacks[caller] = callback;
  
  worker.postMessage(
    target: n,
    caller: caller
  );

您无法运行 web worker 解决方案,因为它需要一个单独的 worker.js 文件来托管 worker 逻辑。

【讨论】:

Ryan,您能否分享链接或解释callback(i,true)callback(i,false) 的作用?我搜索了但找不到我们在这里调用的确切内容。【参考方案4】:

不能同时执行两个循环,记住JS是单线程的。

所以,这样做永远行不通

function loopTest() 
    var test = 0
    for (var i; i<=100000000000, i++) 
        test +=1
    
    return test


setTimeout(()=>
    //This will block everything, so the second won't start until this loop ends
    console.log(loopTest()) 
, 1)

setTimeout(()=>
    console.log(loopTest())
, 1)

如果你想实现多线程,你必须使用 Web Workers,但它们必须有一个单独的 js 文件,你只能将对象传递给它们。

但是,我已经设法通过生成 Blob 文件来使用 Web Workers,而无需分隔文件,并且我也可以向它们传递回调函数。

//A fileless Web Worker
class ChildProcess 
     //@param any ags, Any kind of arguments that will be used in the callback, functions too
    constructor(...ags) 
        this.args = ags.map(a => (typeof a == 'function') ? type:'fn', fn:a.toString() : a)
    

    //@param function cb, To be executed, the params must be the same number of passed in the constructor 
    async exec(cb) 
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('') + 1, wk_string.lastIndexOf(''));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage( callback: cb.toString(), args: this.args );
 
        var resultado = await new Promise((next, error) => 
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        )

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    

    worker() 
        onmessage = async function (e) 
            try                 
                var cb = new Function(`return $e.data.callback`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return $p.fn`)() : p);

                try 
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                 catch (e)  throw new Error(`CallbackError: $e`) 
             catch (e)  postMessage(error: e.message) 
        
    


setInterval(()=>console.log('Not blocked code ' + Math.random()), 1000)

console.log("starting blocking synchronous code in Worker")
console.time("\nblocked");

var proc = new ChildProcess(blockCpu, 43434234);

proc.exec(function(block, num) 
    //This will block for 10 sec, but 
    block(10000) //This blockCpu function is defined below
    return `\n\nbla bla $num\n` //Captured in the resolved promise
).then(function (result)
    console.timeEnd("\nblocked")
    console.log("End of blocking code", result)
)
.catch(function(error)  console.log(error) )

//random blocking function
function blockCpu(ms) 
    var now = new Date().getTime();
    var result = 0
    while(true) 
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
            return;
       

【讨论】:

【参考方案5】:

对于非常长的任务,应该首选 Web-Worker,但是对于足够小的任务(

现在,由于async/await 语法,这可以以更简洁的方式重写。 此外,与其等待setTimeout()(在node-js 中延迟到至少1ms,在第5 次递归调用后延迟到4ms),不如使用MessageChannel。

所以这给了我们

const waitForNextTask = () => 
  const  port1, port2  = waitForNextTask.channel ??= new MessageChannel();
  return new Promise( (res) => 
    port1.addEventListener("message", () => res(),  once: true  );
    port1.start();
    port2.postMessage("");
   );
;

async function doSomethingSlow() 
  const chunk_size = 10000;
  // do something slow, like counting from 0 to Infinity
  for (let i = 0; i < Infinity; i++ ) 
    // we've done a full chunk, let the event-loop loop
    if( i % chunk_size === 0 ) 
      log.textContent = i; // just for demo, to check we're really doing something
      await waitForNextTask();
    
  
  console.log("Ah! Did it!");


console.log("starting my slow computation");
doSomethingSlow();
console.log("started my slow computation");
setTimeout(() => console.log("my slow computation is probably still running"), 5000);
&lt;pre id="log"&gt;&lt;/pre&gt;

【讨论】:

【参考方案6】:

如果您使用的是 jQuery,我创建了 Alnitak's answer 的延迟实现

function deferredEach (arr, batchSize) 

    var deferred = $.Deferred();

    var index = 0;
    function chunk () 
        var lastIndex = Math.min(index + batchSize, arr.length);

        for(;index<lastIndex;index++)
            deferred.notify(index, arr[index]);
        

        if (index >= arr.length) 
            deferred.resolve();
         else 
            setTimeout(chunk, 0);
        
    ;

    setTimeout(chunk, 0);

    return deferred.promise();


然后你就可以使用返回的 promise 来管理进度和完成回调:

var testArray =["Banana", "Orange", "Apple", "Mango"];
deferredEach(testArray, 2).progress(function(index, item)
    alert(item);
).done(function()
    alert("Done!");
)

【讨论】:

【参考方案7】:

使用 ECMA 异步函数很容易编写非阻塞异步代码,即使它执行 CPU 密集型操作。让我们在一个典型的学术任务上执行此操作 - 斐波那契计算以获得令人难以置信的巨大价值。 您所需要的只是插入一个允许不时到达事件循环的操作。使用这种方法,您将永远不会冻结用户界面或 I/O。

基本实现:

const fibAsync = async (n) => 
  let lastTimeCalled = Date.now();

  let a = 1n,
    b = 1n,
    sum,
    i = n - 2;
  while (i-- > 0) 
    sum = a + b;
    a = b;
    b = sum;
    if (Date.now() - lastTimeCalled > 15)  // Do we need to poll the eventloop?
      lastTimeCalled = Date.now();
      await new Promise((resolve) => setTimeout(resolve, 0)); // do that
    
  
  return b;
;

现在我们可以使用它了 (Live Demo):

let ticks = 0;

console.warn("Calulation started");

fibAsync(100000)
  .then((v) => console.log(`Ticks: $ticks\nResult: $v`), console.warn)
  .finally(() => 
    clearTimeout(timer);
  );

const timer = setInterval(
  () => console.log("timer tick - eventloop is not freezed", ticks++),
  0
);

我们可以看到,定时器运行正常,说明事件循环没有阻塞。

我将这些帮助程序的改进实现发布为antifreeze2 npm 包。它在内部使用setImmediate,因此要获得最大性能,您需要为没有本机支持的环境导入 setImmediate polyfill。

Live Demo

import  antifreeze, isNeeded  from "antifreeze2";

const fibAsync = async (n) => 
  let a = 1n,
    b = 1n,
    sum,
    i = n - 2;
  while (i-- > 0) 
    sum = a + b;
    a = b;
    b = sum;
    if (isNeeded()) 
      await antifreeze();
    
  
  return b;
;

【讨论】:

以上是关于如何制作非阻塞的javascript代码?的主要内容,如果未能解决你的问题,请参考以下文章

Javascript Promises库在浏览器中制作“长时间运行代码 - 非阻塞UI”?

如何制作 setVisible 阻塞的非模态对话框?

Javascript 默认是同步(阻塞)还是异步(非阻塞)

java同步非阻塞IO

java同步非阻塞IO

一篇文章快速搞懂JavaScript事件循环(微任务宏任务),同步异步和阻塞非阻塞