【问题标题】:How to make non-blocking javascript code?如何制作非阻塞的javascript代码?
【发布时间】: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()

标签: javascript asynchronous callback nonblocking


【解决方案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;
        };
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-18
          • 2015-11-25
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多