Javascript setTimeout
Overview:
首先弄清setTimeout工作原理,必须先确定几个概念:
GUI渲染线程负责渲染网页,GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起。页面将停止一切解析和渲染的行为 --- 浅析浏览器解析和渲染
这些线程,都会产生不同的异步事件。
针对“单线程”,那么,异步生成的事件都会添加到队列的末尾。
那么setTimeout(function(){...}, 0),这里的0s delay 可以理解为,什么时候执行回调,这里是执行到setTimeout创建个timer,并0s后添加到执行队列(尾)。
注意,这里并不一定就执行了。执行与否,取决于当前调用堆栈是否为空。即Task队列在当前时刻只有timer任务,那么此刻就执行Timer callback function。
Details:
想像一个场景:
"do sth." button 和 一个 显示结果的 result Div,button绑定一个onclick handler:
- 一段运行很久的计算程序(比如,3min)
- div 显示计算结果
然后你点击button,等了3min,3min内看不到任何变化。再点击button,等了1min,你还是看不到任何变化。你又点击……
添加一个显示状态的status DIV,修改onclick handler(function longCal):
- div 显示“caculating” (将会持续3min)
- 执行longCal(运行3min)
- 在result div显示结果
- 在status Div 显示状态“done”
可是,你看不到status Div 出现caculating。
那么把任务分解,每一步都做了什么。
注:浏览器会把所有event生成的TODO 任务 放在一个单队列。而且,重排status Div,显示“caculating” 需要新开一个单独的任务,并放到当前调用队列末尾。
这里的event可以理解为一个个任务,基于任务驱动js引擎执行。js引擎,单线程,源源不断接受新任务(若有),并插入执行队列尾。
- queue:空队列
- event:点击button,把1~4行 添加到队列中(尾部)
- event:执行(这里说“执行”不是很准确,在这里根本没有真正执行,只是由browser engine将当前执行的代码块创建的任务并放到插入执行队列尾,下同)第1行(显示“caculating”)Task,后续任务:执行第2~4行;重排status Div(显示caculating)
注:这里执行到第一行,需要更新DOM,需要由DOM change event 触发另一个线程,因此排到队列末尾 - event:执行2nd行(caculation),后续任务:执行第3~4行;status Div 显示caculating
- event:执行3rd 行(result Div 显示caculated result),后续任务:执行4th行;status Div 显示caculating;result Div 显示result
- event:执行4th行(status Div 显示done),后续任务:status Div 显示caculating;result Div 显示result;status Div 显示done
- event:这时,onclick handle 执行结束,等待下一个task。onclick handle 移出队列,后续任务:status Div 显示caculating;result Div 显示result;status Div 显示done
- event:status Div 显示caculating
- event:result Div 显示result
- event:status Div 显示done
由于执行很快,由caculation→done, 几乎一闪而过。所以感觉根本没有caculating显示。
你可以看出:你预期是想要在执行caculation过程中,status Div 显示caculating的,可以事实却根本没有显示。
How to solve:
使用setTimeout(),0s delay,然后
修改onclick handle:
- div 显示“caculating” (将会持续3min)
- 执行setTimeout,0s timeout,回调函数调用“longCal”:longCal函数同上,只不过没有第一行。
任务分解:
- queue:空
- event:click button,后续任务:更新status Div;执行setTimeout
- event:执行1st行(更新status Div 显示caculating),后续任务:setTimeout;更新status div显示caculating
- event:调用setTimeout,后续任务:更新status div显示caculating 注:假设0s内没有任何其他定时任务
- event:0s后 ,此时任务队列:更新status div显示caculating;执行longCal 函数
- event:更新status div显示caculating,后续任务:执行longCal
注:这里更新 status div显示caculating 会在定时器0s触发回调前执行。创建定时器后,继续执行下一个任务,计时任务交给定时器线程定时。满足timeout后,添加到js引擎执行队列尾,等待执行。
这里的浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时,所以队列中的定时事件也是异步事件. --- 深入理解JavaScript定时机制---定时触发线程 - 同上
可以看出:你能看到预期的caculating。
References:
1. setTimeout with zero delay used often in web pages, why?
4. Why does setTimeout(fn, 0) sometimes help?
5. HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB BROWSERS