事件起因源于对我之前写的一个小工具的再优化,工具遇到的场景大概是这样的:
工具需要在用户点击完按钮后做大量的循环操作(这里暂时没有算法层的优化),大概有 6 万次,然后与此同时,界面上还需要显示出当前循环进行的程度,百分比。大概是这样的:
setInterval(function() {
// trigger progress...
}, 16);
for(var i=0; i<60000; i++) {
// do something...
}
这里在之前就遇到一个问题,就是当 js 去在一个函数里进行循环操作时,js 会被卡住,因为是一个同步过程,所以界面的更新事件会在循环进行完以后再触发执行,这当然不是我们想要的。然后后来,我就把它变成了下面这种递归模式:
function loop() {
// do something ...
setTimeout(loop, 0);
}
这样的话,就把每次循环变成了一个异步事件,等上次循环执行完后,再把下次循环的事件挂到事件队列上,等待下次 event loop 执行。这样就使得界面更新事件可以正常在循环进行中的时候触发了。
然而这样又出现了另一个问题,就是循环执行速度极慢(详情可脑补《疯狂动物城》的树懒。。。)
好吧,其实分析一下就可以得知,当我执行完一次函数的时候,才把下次循环挂到事件队列上,也就是下次循环的执行时间最短是下次 event loop 取事件队列的间隔时间,所以就导致速度很慢。
OK,那我把所有循环都做成异步事件,一次性都挂到事件队列上呢?
for(var i=0; i<total; i++) {
setTimeout((function(count){
return function() {
// do something...
}
})(i), 0);
}
然后呢?我得到的结果是并不能到达我们想象的样子。这些事件会被快速执行,但是界面更新事件只会触发一次,然后就等全部循环执行完成后再次触发,有点儿类似同步循环的阻塞,但是我试了一下,要比同步的阻塞写法慢一些。
好吧,其实这里想一些也就明白了,我这里是将所有的异步事件一次性挂到事件队列上面,导致这一次的事件队列特别长,所以 js 会等这一次的事件全部处理完成后,再去取下次的队列,也就变成了类似同步的写法,只是中间应该会耗费一些事件去切换事件处理吧。
那么再然后呢?怎么样才能做到既能快速进行运算,有不影响到其他事件的触发呢?
最后我试着把循环拆成了若干个小循环去完成,大概是这样:
function loop() {
for(var i=current; i<total && i<current+200; i++) {
// do something...
}
current = i;
setTimeout(loop, 0);
}
这样就是每次异步函数只执行 200 次,然后把下次的循环挂到下次的事件队列里,这样界面更新事件就有机会出现在下次的事件队列中,从而得到触发。
事情就此解决,也从这件事情中,更明白了一些关于异步和 event loop 的东西:
- 单个函数内部的执行是同步的,影响到后面函数的执行
- js 会在全部执行完一次 event loop 的异步事件后,再去取下次的队列,也就是单次队列里面的异步函数如果耗费大量时间的话(不管是因为单个函数耗费大还是因为所有函数个数耗费大),都会影响到下次取队列的时间
- js 是单线程的,即使你是异步写法,到后面也是同步执行,所以要更好的优化性能的话,就尽量的让 js 在每次的 event loop 的间隔时间里,尽可能多的进行运算。如果运算时间实在太长,就合理的拆成小段儿来填充进每一个 event loop 里面
最后这里记篇文章:
How JavaScript Timers Work
是安然君推荐给我的,把 event loop 的运行机制讲的很明白,个人感觉很好,有空儿尝试翻译一下~
嗯嗯,大概就是这样吧,本文其实少了很多图示,(好吧,其实是因为我太懒了。。。),大家可以去看推荐的文章,讲的很清楚。
本文中如果有不对的地方,欢迎指正,一起学习~
嗯嗯,就这样吧,晚安~