最近项目中遇到了一个场景,其实很常见,就是定时获取接口刷新数据。那么问题来了,假设我设置的定时时间为1s,而数据接口返回大于1s,应该用同步阻塞仍是异步?咱们先整理下js中定时器的相关知识,再来看这个问题。
先来简单认识,后面咱们试试用setTimeout 实现 setInterval 的功能
setTimeout(function, milliseconds, param1, param2, ...) clearTimeout() // 阻止定时器运行 e.g. setTimeout(function(){ alert("Hello"); }, 3000); // 3s后弹出
setInterval(function, milliseconds, param1, param2, ...) e.g. setInterval(function(){ alert("Hello"); }, 3000); // 每隔3s弹出
setTimeout和setInterval的延时最小间隔是4ms(W3C在HTML标准中规定);在JavaScript中没有任何代码是马上执行的,但一旦进程空闲就尽快执行。这意味着不管是setTimeout仍是setInterval,所设置的时间都只是n毫秒被添加到队列中,而不是过n毫秒后当即执行。
为了讲清楚这两个抽象的概念,咱们借用阮大大借用的比喻,先来模拟一个场景:javascript
那么:html
再深刻点:java
再再深刻:git
总所周知,JavaScript 这门语言的核心特征,就是单线程(是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个)。这和 JavaScript 最初设计是做为一门 GUI 编程语言有关,最初用于浏览器端,单一线程控制 GUI 是很广泛的作法。但这里特别要划个重点,虽然JavaScript是单线程,但浏览器是多线程的!!!例如Webkit或是Gecko引擎,可能有javascript引擎线程、界面渲染线程、浏览器事件触发线程、Http请求线程,读写文件的线程(例如在Node.js中)。ps:可能要总结一篇浏览器渲染的文章了。github
HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。
以前阮大大写了一篇 《JavaScript 运行机制详解:再谈Event Loop》,而后被 朴灵评注了,特别是同步异步的理解上,两位大牛有很大的歧义。
e.g. alert('立刻能看到我拉'); console.log('也能立刻看到我哦');
e.g. setTimeout(function() { // 过一段时间才能执行我哦 }, 1000);
一个异步过程一般是这样的:主线程发起一个异步请求,相应的工做线程(好比浏览器的其余线程)接收请求并告知主线程已收到(异步函数返回);主线程能够继续执行后面的代码,同时工做线程执行异步任务;工做线程完成工做后,通知主线程;主线程收到通知后,执行必定的动做(调用回调函数)。
e.g. setTimeout(fn, 1000); // setTimeout就是异步过程的发起函数,fn是回调函数
异步过程的通讯机制:工做线程将消息放到消息队列,主线程经过事件循环过程去取消息。
一个先进先出的队列,存放各种消息。
主线程(js线程)只会作一件事,就是从消息队列里面取消息、执行消息,再取消息、再执行。消息队列为空时,就会等待直到消息队列变成非空。只有当前的消息执行结束,才会去取下一个消息。这种机制就叫作事件循环机制 Event Loop,取一个消息并执行的过程叫作一次循环。
工做线程是生产者,主线程是消费者。工做线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。
其实到这儿,应该能很好解释setTimeout(function, 0) 这个经常使用的“奇技淫巧”了。很简单,就是为了将function
里的任务异步执行,0不表明当即执行,而是将任务推到消息队列的最后,再由主线程的事件循环去调用它执行。编程
HTML5 中规定setTimeout 的最小时间不是0ms,而是4ms。
再次强调,定时器指定的时间间隔,表示的是什么时候将定时器的代码添加到 消息队列,而 不是什么时候执行代码。因此真正什么时候执行代码的时间是不能保证的,取决于什么时候被主线程的事件循环取到,并执行。
setInterval(function, N)
那么显而易见,上面这段代码意味着,每隔N秒把function事件推到消息队列中,何时执行?母鸡啊!浏览器
上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,因此等待,some event
执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,因此等待;又过了100ms,理论上又要往队列里推一个定时器代码,但因为此时T2还在队列中,因此T3不会被添加,结果就是此时被跳过;这里咱们能够看到,T1定时器执行结束后立刻执行了T2代码,因此并无达到定时器的效果。多线程
综上所述,setInterval有两个缺点:app
setTimeout(function () { // 任务 setTimeout(arguments.callee, interval); }, interval)
警告:在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 经过要么给函数表达式一个名字,要么使用一个函数声明.
上述函数每次执行的时候都会建立一个新的定时器,第二个setTimeout使用了arguments.callee()获取当前函数的引用,而且为其设置另外一个定时器。好处:异步
回顾最开始的业务场景的问题,用同步阻塞仍是异步,答案已经出来了...
PS:其实还有macrotask与microtask等知识点没有提到,总结了那么多,其实JavaScript深刻下去还有不少,任重而道远呀。
参考:
【译】JavaScript 如何工做的: 事件循环和异步编程的崛起 + 5 个关于如何使用 async/await 编写更好的技巧
已同步至我的博客- 软硬皆施
Github 欢迎star :)