簡體   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