简体   繁体   English

如何制作非阻塞 javascript 代码?

[英]How to make non-blocking javascript code?

How can I make a simple, non-block Javascript function call?如何进行简单的非阻塞 Javascript function 调用? For example:例如:

  //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);
  }

outputs产出

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

How can I form this simple loop to execute asynchronously and output the results via a callback function?我怎样才能形成这个简单的循环来异步执行 output 通过回调 function 结果? The idea is to not block "do more stuff":这个想法是不阻止“做更多的事情”:

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

I've tried following tutorials on callbacks and continuations, but they all seem to rely on external libraries or functions.我已经尝试按照有关回调和延续的教程进行操作,但它们似乎都依赖于外部库或函数。 None of them answer the question in a vacuum: how does one write Javascript code to be non-blocking??他们都没有真空地回答这个问题:一个人如何编写 Javascript 代码是非阻塞的?


I have searched very hard for this answer before asking;在提问之前,我已经非常努力地寻找这个答案; please don't assume I didn't look.请不要以为我没看。 Everything I found is Node.js specific ( [1] , [2] , [3] , [4] , [5] ) or otherwise specific to other functions or libraries ( [6] , [7] , [8] , [9] , [10] , [11] ), notably JQuery and setTimeout() .我发现的所有内容都是 Node.js 特定的( [1][2][3][4][5] )或其他特定于其他函数或库( [6][7][8][9 ] ][10][11] ),特别是 JQuery 和setTimeout() Please help me write non-blocking code using Javascript , not Javascript-written tools like JQuery and Node.请帮助我使用Javascript编写非阻塞代码,而不是像 JQuery 和 Node.js 这样的 Javascript 编写的工具。 Kindly reread the question before marking it as duplicate.在将其标记为重复之前,请重读该问题。

To make your loop non-blocking, you must break it into sections and allow the JS event processing loop to consume user events before carrying on to the next section.为了让你的循环不被阻塞,你必须把它分成几个部分,让 JS 事件处理循环在继续下一部分之前使用用户事件。

The easiest way to achieve this is to do a certain amount of work, and then use setTimeout(..., 0) to queue the next chunk of work.实现这一点的最简单方法是做一定数量的工作,然后使用setTimeout(..., 0)将下一个工作块排队。 Crucially, that queueing allows the JS event loop to process any events that have been queued in the meantime before going on to the next piece of work:至关重要的是,该排队允许 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);
        }
    })();
}

with usage:用法:

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

See http://jsfiddle.net/alnitak/x3bwjjo6/ for a demo where the callback function just sets a variable to the current iteration count, and a separate setTimeout based loop polls the current value of that variable and updates the page with its value.有关演示,请参阅http://jsfiddle.net/alnitak/x3bwjjo6/的演示,其中callback函数仅将变量设置为当前迭代计数,并且单独的基于setTimeout的循环轮询该变量的当前值并使用其值更新页面.

SetTimeout with callbacks is the way to go.带有回调的 SetTimeout 是要走的路。 Though, understand your function scopes are not the same as in C# or another multi-threaded environment.但是,请了解您的函数作用域与 C# 或其他多线程环境中的不同。

Javascript does not wait for your function's callback to finish. Javascript 不会等待函数的回调完成。

If you say:如果你说:

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

Your alert will fire before the function you passed will.您的警报将在您传递的函数之前触发。

The difference being that alert blocked the thread, but your callback did not.不同之处在于警报阻止了线程,但您的回调没有。

There are in general two ways to do this as far as I know.据我所知,通常有两种方法可以做到这一点。 One is to use setTimeout (or requestAnimationFrame if you are doing this in a supporting environment).一种是使用setTimeout (或requestAnimationFrame如果您在支持环境中执行此操作)。 @Alnitak shown how to do this in another answer. @Alnitak 在另一个答案中展示了如何做到这一点。 Another way is to use a web worker to finish your blocking logic in a separate thread, so that the main UI thread is not blocked.另一种方法是使用 web worker 在一个单独的线程中完成你的阻塞逻辑,这样主 UI 线程就不会被阻塞。

Using requestAnimationFrame or setTimeout :使用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(); }

Using web worker:使用网络工作者:

 /***** 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 }); }

You cannot run the web worker solution as it requires a separate worker.js file to host worker logic.您无法运行 Web Worker 解决方案,因为它需要一个单独的worker.js文件来承载工作器逻辑。

You cannot execute Two loops at the same time, remember that JS is single thread.不能同时执行两个循环,记住 JS 是单线程的。

So, doing this will never work所以,这样做永远不会奏效

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)

If you want to achieve multi thread you have to use Web Workers , but they have to have a separated js file and you only can pass objects to them.如果你想实现多线程,你必须使用 Web Workers ,但他们必须有一个单独的 js 文件,你只能将对象传递给他们。

But, I've managed to use Web Workers without separated files by genering Blob files and i can pass them callback functions too.但是,我已经通过生成 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; } }

For very long tasks, a Web-Worker should be preferred, however for small-enough tasks (< a couple of seconds) or for when you can't move the task to a Worker (eg because you needs to access the DOM or whatnot, Alnitak's solution of splitting the code in chunks is the way to go.对于非常长的任务,Web-Worker 应该是首选,但是对于足够小的任务(< 几秒钟)或当您无法将任务移动到 Worker 时(例如,因为您需要访问 DOM 或诸如此类的) , Alnitak 将代码分成块的解决方案是要走的路。

Nowadays, this can be rewritten in a cleaner way thanks to async/await syntax.如今,由于async/await语法,这可以以更简洁的方式重写。
Also, instead of waiting for setTimeout() (which is delayed to at least 1ms in node-js and to 4ms everywhere after the 5th recursive call), it's better to use aMessageChannel .此外,与其等待setTimeout() (在 node-js 中至少延迟到 1ms,在第 5 次递归调用后延迟到 4ms),不如使用MessageChannel

So this gives us所以这给了我们

 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);
 <pre id="log"></pre>

Using ECMA async function it's very easy to write non-blocking async code, even if it performs CPU-bound operations.使用 ECMA async function 可以很容易地编写非阻塞异步代码,即使它执行的是 CPU 密集型操作。 Let's do this on a typical academic task - Fibonacci calculation for the incredible huge value.让我们在一个典型的学术任务上做这个——斐波那契计算令人难以置信的巨大价值。 All you need is to insert an operation that allows the event loop to be reached from time to time.你只需要插入一个允许时不时到达事件循环的操作。 Using this approach, you will never freeze the user interface or I/O.使用这种方法,您将永远不会冻结用户界面或 I/O。

Basic implementation:基本实现:

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;
};

And now we can use it ( 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
);

As we can see, the timer is running normally, which indicates the event loop is not blocking.正如我们所见,计时器正常运行,这表明事件循环没有阻塞。

I published an improved implementation of these helpers as antifreeze2 npm package. It uses setImmediate internally, so to get the maximum performance you need to import setImmediate polyfill for environments without native support.我将这些助手的改进实现发布为antifreeze2 npm package。它在内部使用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;
};

If you are using jQuery, I created a deferred implementation of Alnitak's answer如果您使用的是 jQuery,我创建了Alnitak 答案的延迟实现

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();

}

Then you'll be able to use the returned promise to manage the progress and done callback:然后你就可以使用返回的 promise 来管理进度和完成回调:

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

I managed to get an extremely short algorithm using functions.我设法使用函数得到了一个非常短的算法。 Here is an example:这是一个例子:

let l=($,a,f,r)=>{f(r||0),$((r=a(r||0))||0)&&l($,a,f,r)};

l
  (i => i < 4, i => i+1, console.log) 

/*
output:
0
1
2
3
*/

I know this looks very complicated, so let me explain what is really going on here.我知道这看起来很复杂,所以让我解释一下这里到底发生了什么。

Here is a slightly simplified version of the l function.这是 l function 的稍微简化的版本。

let l_smpl = (a,b,c,d) => {c(d||0);d=b(d||0),a(d||0)&&l_smpl(a,b,c,d)||0}

First step in the loop, l_smpl calls your callback and passes in d - the index.循环的第一步,l_smpl 调用您的回调并传入 d - 索引。 If d is undefined, as it would be on the first call, it changes it to 0.如果 d 未定义,就像第一次调用时一样,它会将其更改为 0。

Next, it updates d by calling your updater function and setting d to the result.接下来,它通过调用您的更新程序 function 并将 d 设置为结果来更新 d。 In our case, the updater function would add 1 to the index.在我们的例子中,更新程序 function 会将索引加 1。

The next step checks if your condition is met by calling the first function and checking if the value is true meaning the loop is not done.下一步通过调用第一个 function 并检查该值是否为真意味着循环未完成来检查是否满足您的条件。 If so, it calls the function again, or otherwise, it returns 0 to end the loop.如果是,则再次调用 function,否则返回 0 结束循环。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM