如何制作非阻塞的javascript代码?
Posted
技术标签:
【中文标题】如何制作非阻塞的javascript代码?【英文标题】:How to make non-blocking javascript code? 【发布时间】:2014-12-24 07:23:54 【问题描述】:如何进行简单的非阻塞 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]、[2]、[3]、[4]、[5])或其他特定于其他函数或库的内容([6]、[7]、[8]、 [9]、[10]、[11]),尤其是 JQuery 和 setTimeout()
。请帮助我使用 Javascript 编写非阻塞代码,而不是像 JQuery 和 Node 这样的 Javascript 编写的工具。 请在将其标记为重复之前重新阅读该问题。
【问题讨论】:
毫不费力。您实际上必须告诉线程休眠一段时间才能阻塞线程。为避免睡眠,请使用带有回调的计时器。 sitepoint.com/settimeout-example 没有办法做到这一点。 Javascript 不是多线程的,只能对任务进行排队。您可以稍后执行长时间运行的任务,但不能与其他任务同时执行。 @AndrewHoffman 我不确定你是否理解。你不能让 JS 进入睡眠状态,但你可以让它忙到 UI 循环不能服务任何事件。 你可以用警报之类的东西来阻止线程,我希望每个浏览器都会禁用它。糟糕的程序员冻结了我的浏览器。 -_-' 在mozilla developer network 中搜索fork()
或exec()
或pthread()
,你会发现是空的。为什么?因为对子进程和线程的支持不是浏览器 javascript 的标准功能。 Web workers 是一项实验性功能,旨在创建可以通信但不共享范围的其他进程。不支持按照您的建议同时运行 CPU 代码。实际上,引用的所有“异步”JS 代码都是关于 I/O 事件的。在 I/O 上:blah()
【参考方案1】:
为了让你的循环不阻塞,你必须把它分成几个部分,让 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
的循环轮询该变量的当前值并使用其值更新页面。
【讨论】:
感谢您将这样的工作纳入您的答案,但是(就像您在评论中提到的那样)for
循环只是一个模拟需要很长时间的虚拟函数。除非我误解了某些东西,否则这段代码只对那种特殊情况有效。
@user1717828 哦,好吧。简短的回答是,你不能像你一样写你的三行并期望它工作 - 你必须调用你的长时间运行的任务(异步),然后安排在完成时调用另一个函数,就像我做的那样在我的yieldingLoop
示例中使用finished
回调。原程序流程将不间断地进行。
执行过程中能不能在屏幕上画点东西?【参考方案2】:
带有回调的 SetTimeout 是要走的路。不过,请了解您的函数范围与 C# 或其他多线程环境中的不同。
Javascript 不会等待函数的回调完成。
如果你说:
function doThisThing(theseArgs)
setTimeout(function (theseArgs) doThatOtherThing(theseArgs); , 1000);
alert('hello world');
您的警报将在您传递的函数之前触发。
不同之处在于警报阻止了线程,但您的回调没有。
【讨论】:
比起@Alnitak,我更喜欢这个答案的清晰性。但是,正如@Alnitak 指出的那样,值得注意的是,也可以使用setTimeout(..., 0)
来避免不必要的等待时间。它仍然是非阻塞的!
我同意,setTimeout(..., 0) 有助于在事件调用堆栈空闲时避免不必要的延迟。【参考方案3】:
据我所知,一般有两种方法可以做到这一点。一种是使用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
文件来托管 worker 逻辑。
【讨论】:
Ryan,您能否分享链接或解释callback(i,true)
和callback(i,false)
的作用?我搜索了但找不到我们在这里调用的确切内容。【参考方案4】:
不能同时执行两个循环,记住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;
【讨论】:
【参考方案5】:对于非常长的任务,应该首选 Web-Worker,但是对于足够小的任务(
现在,由于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>
【讨论】:
【参考方案6】:如果您使用的是 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!");
)
【讨论】:
【参考方案7】:使用 ECMA 异步函数很容易编写非阻塞异步代码,即使它执行 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;
;
现在我们可以使用它了 (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;
;
【讨论】:
以上是关于如何制作非阻塞的javascript代码?的主要内容,如果未能解决你的问题,请参考以下文章