setTimeout,前端工程师一定会打交道的一个函数.它看上去很是的简单,朴实.有着一个很不平凡的名字--定时器.让年少的我天真的觉得本身能够操纵将来.殊不知朴实之中隐含着惊天大密.我还记得我第一次用这个函数的时候,我天真的觉得它就是js实现多线程的工具.当时用它实现了一个坦克大战的小游戏,玩儿不亦乐乎.但是随着在前端这条路上越走越远,对它理解开始产生了变化.它彷佛开始蒙上了面纱,时常有一些奇怪的表现让我捉摸不透.终于,个人耐心耗尽,下定决心,要撕开它的面具,一探究竟.javascript
要说setTimeout的渊源,就得从它的官方定义提及.w3c是这么定义的html
setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。前端
看到这样一个说明,咱们明白了它就是一个定时器,咱们设定的函数就是一个"闹钟",时间到了它就会去执行.然而聪明的你不由有这样一个疑问,若是是settimeout(fn,0)呢?按照定义的说明,它是否会立马执行?实践是检验真理的惟一标准,让咱们来看看下面的实验java
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script> alert(1); setTimeout("alert(2)", 0); alert(3); </script> </body> </html>
这是一个很简单的实验,若是settimeout(0)会当即执行,那么这里的执行结果就应该是1->2>3 . 然而实际的结果倒是1->3->2. 这说明了settimeout(0)并非当即执行.同时让咱们对settimeout的行为感到很诡异.ajax
咱们先把上面的问题放一放.从js语言的设计上来看看是否能找到蛛丝马迹.编程
咱们发现js语言设计的一个很重要的点是,js是没有多线程的.js引擎的执行是单线程执行.这个特性曾经困扰我好久,我想不明白既然js是单线程的,那么是谁来为定时器计时的?是谁来发送ajax请求的?我陷入了一个盲区.即将js等同于浏览器.咱们习惯了在浏览器里面执行代码,却忽略了浏览器自己.js引擎是单线程的,但是浏览器却能够是多线程的,js引擎只是浏览器的一个线程而已.定时器计时,网络请求,浏览器渲染等等.都是由不一样的线程去完成的. 口说无凭,我们依然看一个例子浏览器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> <script> var isEnd = true; window.setTimeout(function () { isEnd = false;//1s后,改变isEnd的值 }, 1000); while (isEnd); alert('end'); </script> </html>
isEnd默认是true的,在while中是死循环的.最后的alert是不会执行的. 我添加了一个定时器,1秒后将isEnd改成false. 若是说js引擎是多线程的,那么在1秒后,alert就会被执行.然而实际状况是,页面会永远死循环下去.alert并无执行.这很好的证实了,settimeout并不能做为多线程使用.js引擎执行是单线程的.网络
从上面的实验中,咱们更加疑惑了,settimeout到底作了什么事情呢?前端工程师
原来仍是得从js语言的设计上寻找答案.多线程
js引擎单线程执行的,它是基于事件驱动的语言.它的执行顺序是遵循一个叫作事件队列的机制.从图中咱们能够看出,浏览器有各类各样的线程,好比事件触发器,网络请求,定时器等等.线程的联系都是基于事件的.js引擎处理到与其余线程相关的代码,就会分发给其余线程,他们处理完以后,须要js引擎计算时就是在事件队列里面添加一个任务. 这个过程当中,js并不会阻塞代码等待其余线程执行完毕,并且其余线程执行完毕后添加事件任务告诉js引擎执行相关操做.这就是js的异步编程模型.
如此咱们再回过头来看settimeout(0)就会恍然大悟.js代码执行到这里时,会开启一个定时器线程,而后继续执行下面的代码.该线程会在指定时间后往事件队列里面插入一个任务.由此可知settimeout(0)里面的操做会放在全部主线程任务以后. 这也就解释了为何第一个实验结果是1->3-2 .
因而可知官方对于settimeout的定义是有迷惑性的.应该给一个新的定义:
在指定时间内, 将任务放入事件队列,等待js引擎空闲后被执行.
谈到这里,就不得不说浏览器的另一个引擎---GUI渲染引擎. 在js中渲染操做也是异步的.好比dom操做的代码会在事件队列中生成一个任务,js执行到这个任务时就会去调用GUI引擎渲染.
js语言设定js引擎与GUI引擎是互斥的,也就是说GUI引擎在渲染时会阻塞js引擎计算.缘由很简单,若是在GUI渲染的时候,js改变了dom,那么就会形成渲染不一样步. 咱们须要深入理解js引擎与GUI引擎的关系,由于这与咱们平时开发息息相关,咱们时长会遇到一些很奇葩的渲染问题.看这个例子
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> <script> function long_running(status_div) { var result = 0; for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } document.querySelector(status_div).innerHTML = 'calclation done' ; } document.querySelector('#do').onclick = function () { document.querySelector('#status').innerHTML = 'calculating....'; long_running('#status'); }; document.querySelector('#do_ok').onclick = function () { document.querySelector('#status_ok').innerHTML = 'calculating....'; window.setTimeout(function (){ long_running('#status_ok') }, 0); }; </script> </body> </html>
咱们但愿能看到计算的每个过程,咱们在程序开始,计算,结束时,都执行了一个dom操做,插入了表明当前状态的字符串,Not Calculating yet.和calculating....和calclation done.计算中是一个耗时的3重for循环. 在没有使用settimeout的时候,执行结果是由Not Calculating yet 直接跳到了calclation done.这显然不是咱们但愿的.而形成这样结果的缘由正是js的事件循环单线程机制.dom操做是异步的,for循环计算是同步的.异步操做都会被延迟到同步计算以后执行.也就是代码的执行顺序变了.calculating....和calclation done的dom操做都被放到事件队列后面并且紧跟在一块儿,形成了丢帧.没法实时的反应.这个例子也告诉了咱们,在须要实时反馈的操做,如渲染等,和其余相关同步的代码,要么一块儿同步,要么一块儿异步才能保证代码的执行顺序.在js中,就只能让同步代码也异步.即给for计算加上settimeout.
不一样浏览器的实现状况不一样,HTML5定义的最小时间间隔是4毫秒. 使用settimeout(0)会使用浏览器支持的最小时间间隔.因此当咱们须要把一些操做放到下一帧处理的时候,咱们一般使用settimeout(0)来hack.
这个函数与settimeout很类似,但它是专门为动画而生的.settimeout常常被用来作动画.咱们知道动画达到60帧,用户就没法感知画面间隔.每一帧大约16毫秒.而requestAnimationFrame的帧率恰好是这个频率.除此以外相比于settimeout,还有如下的一些优势: