如何制作非阻塞javascript代码?

kcwpcxri  于 2023-01-19  发布在  Java
关注(0)|答案(8)|浏览(266)

如何进行简单的非阻塞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(https://stackoverflow.com/questions/2878008/how-do-i-create-a-non-blocking-asynchronous-function-in-node-js)、2(https://stackoverflow.com/questions/23757363/how-to-create-non-blocking-asynchronous-function-in-node-js-and-express-js)、3(https://stackoverflow.com/questions/9362823/why-is-a-function-and-a-callback-non-blocking-in-node-js)、4(https://stackoverflow.com/questions/5670190/how-do-i-write-non-blocking-code-in-node-js)、5(https://stackoverflow.com/questions/9546225/nodejs-how-to-create-a-non-blocking-computation))或其他特定于其他函数或库的函数或库([6],7(https://stackoverflow.com/questions/7729382/how-to-make-a-non-blocking-sleep-in-javascript-jquery),8(https://stackoverflow.com/questions/6880392/are-jquery-fadein-animation-functions-non-blocking),9(https://stackoverflow.com/questions/15855149/non-blocking-script-not-working-with-jquery),10(https://stackoverflow.com/questions/3845972/whats-the-best-way-to-implement-a-sleep-function-javascript),11(https://stackoverflow.com/questions/10180391/javascript-how-to-avoid-blocking-the-browser-while-doing-heavy-work)),特别是JQuery和setTimeout()。请帮助我使用 Javascript 编写非阻塞代码,而不是JQuery和Node等Javascript编写的工具。请在标记为重复之前重新阅读问题。

polkgigr

polkgigr1#

要使循环不阻塞,必须将其分成几个部分,并允许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的循环轮询该变量的当前值,并用其值更新页面。

q1qsirdb

q1qsirdb2#

带回调的SetTimeout是一种可行的方法。但是,要知道你的函数作用域与C#或其他多线程环境中的不同。
Javascript不会等待函数的回调完成。
如果你说:

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

您的警报将在您传递的函数之前触发。
不同之处在于警报阻塞了线程,但回调没有阻塞。

bjg7j2ky

bjg7j2ky3#

据我所知,通常有两种方法可以做到这一点。一种是使用setTimeout(或者requestAnimationFrame,如果你是在一个支持环境中这样做的话)。@Alnitak在另一个答案中展示了如何做到这一点。另一种方法是使用一个web工作者在一个单独的线程中完成你的阻塞逻辑,这样主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();
}

使用Web辅助进程:

/***** 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.js文件来承载辅助进程逻辑。

41zrol4v

41zrol4v4#

你不能同时执行两个循环,记住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;
    }   
}
ars1skjm

ars1skjm5#

对于非常长的任务,Web-Worker应该是首选,但是对于足够小的任务(〈几秒)或者当您无法将任务移动到Worker时(例如,因为您需要访问DOM或诸如此类的东西,Alnitak的将代码拆分为块的解决方案是可行的方法)。
现在,由于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);
<pre id="log"></pre>
laik7k3q

laik7k3q6#

使用ECMA异步函数,即使它执行CPU绑定的操作,也很容易编写非阻塞异步代码。让我们在一个典型的学术任务上做这个-,计算令人难以置信的巨大值的Fibonacci。您所需要的只是插入一个操作,允许不时地到达事件循环。使用这种方法,您永远不会冻结用户界面或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;
};
368yc8dk

368yc8dk7#

如果您使用的是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来管理进度和done回调:

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

brgchamk8#

我设法用函数得到了一个非常短的算法。下面是一个例子:

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
*/

我知道这看起来很复杂,所以让我解释一下到底是怎么回事。
下面是l函数的一个稍微简化的版本。

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

循环的第一步,l_smpl调用回调函数并传入d -索引。如果d未定义,就像第一次调用时一样,它会将其更改为0。
接下来,它通过调用updater函数更新d,并将d设置为结果,在我们的例子中,updater函数会将索引加1。
下一步检查条件是否满足,方法是调用第一个函数,并检查值是否为true,即循环未完成。如果是,则再次调用函数,否则返回0以结束循环。

相关问题