乐趣区

JS单线程与setTimeout执行原理

Javascript 引擎单线程机制

首先明确,JavaScript 引擎是单线程机制。
JavaScript 是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个任务队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为“阻塞式执行”。
可以理解为:只有在 JS 线程中没有任何同步代码要执行的前提下才会执行异步代码
所以一次鼠标点击,或是计时器到达时间点,或是 Ajax 请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就 执行。假如当前 JavaScript 线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

浏览器的多线程机制与事件循环(event loop)

首先明确,浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:

javascript 引擎线程
GUI 渲染线程
浏览器事件触发线程

JavaScript 引擎是单线程运行的, 浏览器无论在什么时候都只且只有一个线程在运行 JavaScript 程序
javascript 引擎是基于事件驱动单线程执行的,JS 引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个 JS 线程在运行 JS 程序。

GUI 渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流 (reflow) 时, 该线程就会执行。但需要注意 GUI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可来自 JavaScript 引擎当前执行的代码块如 setTimeOut,也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)
事件循环(event loop): 是用来管理我们的异步代码的,它会把它们放在一个线程池当中

JavaScript 中 setTimeout 的实现原理

首先明确,setTimeout 函数是异步代码,但其实 setTimeout 并不是真正的异步操作
由于 JS 线程的工作机制:当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout 是异步代码,所以 setTimeout 只能等 js 空闲才会执行
前面提到过,如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。
也就是说 setTimeout 只能保证在指定的时间过后将任务 (需要执行的函数) 插入队列等候,并不保证这个任务在什么时候执行。执行 javascript 的线程会在空闲的时候,自行从队列中取出任务然后执行它。javascript 通过这种队列机制,给我们制造一个异步执行的假象。
有时 setTimeout 中的代码会很快得到执行,我们会感觉这段代码是在异步执行,这是因为 javascript 线程并没有因为什么耗时操作而阻塞,所以可以很快地取出排队队列中的任务然后执行它。

实例分析
在具备了上述理论基础之后,我们对以下几个实例进行分析:

===========================================
var t = true;

window.setTimeout(function (){
t = false;
},1000);

while (t){}

alert(‘end’);
运行结果:程序陷入死循环,t = false 得不到执行,因此 alert(‘end’) 不会执行。解析:

JS 是单线程的,所以会先执行 while(t){} 再 alert,但这个循环体是死循环,所以永远不会执行 alert。
为什么不执行 setTimeout?是因为 JS 的工作机制是:当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout 是异步代码,所以 setTimeout 只能等 JS 空闲才会执行,但死循环是永远不会空闲的,所以 setTimeout 也永远得不到执行。

===========================================
var start = new Date();

setTimeout(function(){
var end = new Date();
console.log(“Time elapsed: “, end – start, “ms”);
}, 500);

while (new Date – start <= 1000){}
运行结果:”Time elapsed: 1035 ms” (这里的 1035 不准确 但是一定是大于 1000 的) 解析:

JS 是单线程 setTimeout 异步代码 其回调函数执行必须需等待主线程运行完毕
当 while 循环因为时间差超过 1000ms 跳出循环后,setTimeout 函数中的回调才得以执行

===========================================
for(var i=0;i<10;i++){
setTimeout(function() {
console.log(i);
}, 0);
}

运行结果:输出 10 个 10 解析:JS 单线程 setTimeout 异步代码 任务队列 问:如何修改可以使上述代码输出 0123456789 自执行函数 或 使用 ES6 中的 let 关键字
// 自执行函数 形成闭包 记忆其被创建时的环境
for(var i=0;i<10;i++){
setTimeout((function() {
console.log(i);
})(), 0);
}

setTimeout(0)函数的作用
现在我们了解了 setTimeout 函数执行的原理,那么它有什么作用呢?setTimeout 函数增加了 Javascript 函数调用的灵活性,为函数执行顺序的调度提供极大便利。简言之,改变顺序,这正是 setTimeout(0)的作用。
使用场景示例:
<input type=”text” onkeydown=”show(this.value)”>
<div></div>
<script type=”text/javascript”>
function show(val) {
document.getElementsByTagName(‘div’)[0].innerHTML = val;
}
</script>
这里绑定了 keydown 事件,意图是当用户在文本框里输入字符时,将输入的内容实时地在 <div> 中显示出来。但是实际效果并非如此,可以发现,每按下一个字符时,<div> 中只能显示出之前的内容,无法得到当前的字符。
修改代码:
<input type=”text” onkeydown=”var self=this; setTimeout(function(){show(self.value)}, 0)”>
<div></div>
<script type=”text/javascript”>
function show(val) {
document.getElementsByTagName(‘div’)[0].innerHTML = val;
}
</script>
这段代码使用 setTimeout(0)就可以实现需要的效果了。
这里其实涉及 2 个任务,1 个是将键盘输入的字符回写到输入框中,一个是获取文本框的值将其写入 div 中。第一个是浏览器自身的默认行为,一个是我们自己编写的代码。很显然,必须要先让浏览器将字符回写到文本框,然后我们才能获取其内容写到 div 中。改变顺序,这正是 setTimeout(0)的作用。
其他应用场景:有时候,加载一些广告的时候,我们用 setTimeout 实现异步,好让广告不会阻塞我们页面的渲染。
setTimeout 和 setInterval 在执行异步代码的时候有着根本的不同

如果一个计时器被阻塞而不能立即执行,它将延迟执行直到下一次可能执行的时间点才被执行(比期望的时间间隔要长些)
如果 setInterval 回调函数的执行时间将足够长(比指定的时间间隔长),它们将连续执行并且彼此之间没有时间间隔。

退出移动版