如何进行简单的非块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"
如何形成这个简单的循环以异步执行并通过回调函数输出结果?我们的想法是不阻止&#34;做更多事情&#34;:
"beginPage"
"do more stuff"
"0 incremented to 10000000"
我已经尝试过关于回调和延续的教程,但它们似乎都依赖于外部库或函数。他们都没有在真空中回答这个问题:如何将Javascript代码写成非阻塞的??
我在询问之前非常努力地寻找这个答案;请不要以为我没看。我发现的一切都是Node.js特定的([1],[2],[3],[4],[5])或其他特定于其他函数或库({{ 3}},[6],[7],[8],[9],[10]),尤其是JQuery和setTimeout()
。请帮助我使用 Javascript 编写非阻塞代码,而不是使用Javascript和Node等Javascript编写的工具。 在将问题标记为重复之前,请重新阅读该问题。
答案 0 :(得分:20)
要使循环无阻塞,必须将其分解为多个部分,并允许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
});
有关callback
函数只是将变量设置为当前迭代计数的演示,请参阅http://jsfiddle.net/alnitak/x3bwjjo6/,并且基于setTimeout
的单独循环轮询该变量的当前值并更新页面及其价值。
答案 1 :(得分:6)
带回调的SetTimeout是要走的路。但是,要了解您的功能范围与C#或其他多线程环境中的功能范围不同。
Javascript不会等待你的函数的回调完成。
如果你说:
function doThisThing(theseArgs) {
setTimeout(function (theseArgs) { doThatOtherThing(theseArgs); }, 1000);
alert('hello world');
}
在你传递的功能出现之前,你的警报会被激活。
区别在于警报阻止了线程,但回调没有。
答案 2 :(得分:1)
你不能同时执行两个循环,记住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;
}
}
答案 3 :(得分:1)
据我所知,通常有两种方法可以做到这一点。一种是使用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.js
文件来托管工作逻辑。
答案 4 :(得分:1)
对于很长的任务,应该首选 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>
答案 5 :(得分:0)
如果您使用的是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来管理进度和已完成的回调:
var testArray =["Banana", "Orange", "Apple", "Mango"];
deferredEach(testArray, 2).progress(function(index, item){
alert(item);
}).done(function(){
alert("Done!");
})