繁体   English   中英

如何制作非阻塞 javascript 代码?

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

如何进行简单的非阻塞 Javascript function 调用? 例如:

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

我怎样才能形成这个简单的循环来异步执行 output 通过回调 function 结果? 这个想法是不阻止“做更多的事情”:

"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.js 这样的 Javascript 编写的工具。 在将其标记为重复之前,请重读该问题。

为了让你的循环不被阻塞,你必须把它分成几个部分,让 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的循环轮询该变量的当前值并使用其值更新页面.

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

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

如果你说:

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

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

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

据我所知,通常有两种方法可以做到这一点。 一种是使用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文件来承载工作器逻辑。

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

对于非常长的任务,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>

使用 ECMA async function 可以很容易地编写非阻塞异步代码,即使它执行的是 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;
};

现在我们可以使用它了( 现场演示):

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 package。它在内部使用setImmediate ,因此要获得最大性能,您需要为没有本机支持的环境导入 setImmediate polyfill

现场演示

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

如果您使用的是 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();

}

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

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

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

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

循环的第一步,l_smpl 调用您的回调并传入 d - 索引。 如果 d 未定义,就像第一次调用时一样,它会将其更改为 0。

接下来,它通过调用您的更新程序 function 并将 d 设置为结果来更新 d。 在我们的例子中,更新程序 function 会将索引加 1。

下一步通过调用第一个 function 并检查该值是否为真意味着循环未完成来检查是否满足您的条件。 如果是,则再次调用 function,否则返回 0 结束循环。

暂无
暂无

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

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