[英]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 线程就不会被阻塞。
使用requestAnimationFrame
或setTimeout
:
//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.