众所周知,JavaScript是单线程的,那么到底什么是单线程呢?今天我们就用setTimeout()举例,看看单线程到底是什么样的

单线程,从名字就能知道,它只有一个主线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

对于js来说,它是一门单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行。

举个例子,就像大家去超市买东西一样,所有买东西的人都需要在收银台排队结账,正常情况下每个收银台同一时间只能为一位顾客结账,这位顾客结账完成才能为下一位顾客服务。

光看文字概念可能不好理解,下面看一段代码:

setTimeout(function(){
 console.log(1);
}, 0);
console.log(2);
console.log(3);

猜猜这段代码的运行结果是什么,会是 1 2 3 吗?

其实运行结果是 2 3 1;这是为什么呢?

这就和setTimeout()的规则有关了。

js代码执行遇到setTimeout(fn,millisec)时,会把fn这个函数放在任务队列中,当js引擎线程空闲时并达到millisec指定的时间时,才会把fn放到js引擎线程中执行。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout() 

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

也就是说,对于下面这个语句:

setTimeout(function(){
 console.log(1);
}, 0);

因为HTML的标准对最短时间间隔有规定,因此它并不会立即执行,而是要等待一段时间。

又因为JS属于单线程语言,这条语句被放入任务队列, console.log(2) 、console.log(3)则先执行,当setTimeout()所设定的时间到了之后,由于前两条语句还未执行完,这时候就会等它们执行完之后,即等执行栈的语句全执行完,才会继续执行setTimeout()语句。

到这里,大家应该也就理解什么叫单线程了。但是肯定有人会想,既然JavaScript是一门单线程语言,任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死,功能也会被限制很多,那为什么还有那么多人用呢?

这里就需要介绍一下JavaScript的异步了。

可上文说了,JavaScript属于单线程语言,单线程怎么会有异步呢?

说起来,JavaScript还是沾了浏览器的光。

浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。

  • javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
  • GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码

对于那些耗时的任务,浏览器给它们单独开了一个线程。

既然浏览器为网络请求这样的异步任务单独开了一个线程,问题似乎得到了解决。但新的问题又来了:这些异步任务完成后,主线程怎么知道呢?答案就是回调函数,整个程序是事件驱动的,每个事件都会绑定相应的回调函数。

这里依旧是用上文的setTimeout()语句举例:

setTimeout(function(){
 console.log(1);
}, 50);//将上文中的0改为50ms

执行这段代码的时候,浏览器异步执行计时操作,当50ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是通过这样的一个个事件驱动起来的。
所以说,js是一直是单线程的,浏览器才是实现异步的那个大佬。JS只不过抱住了浏览器大哥的大腿子,这才有了它强大的战力。

既然那些耗时的任务被浏览器另开了一个线程,那么JS的主线程在做什么呢?

我们来看一张图(图片来自:《Help, I'm stuck in an event-loop》):

JavaScript——从setTimeout()的执行了解js的单线程和异步-LMLPHP

WebAPIs:浏览器为异步任务单独开辟的线程。

callback queue:任务队列

heap:堆

stack:栈

从图中我们可以看到,我们所说的主线程就是有虚线组成的那一部分,堆(heap)和栈(stack)共同组成了js主线程,函数的执行就是通过进栈和出栈实现的,比如图中有一个foo()函数,主线程把它推入栈中,在执行函数体时,发现还需要执行上面的那几个函数,所以又把这几个函数推入栈中,等到函数执行完,就让函数出栈。等到stack清空时,说明一个任务已经执行完了,这时就会从callback queue中寻找下一个人任务推入栈中(这个寻找的过程,叫做event loop,因为它总是循环的查找任务队列里是否还有任务)

也就是说,JS一直在从任务队列中提取任务,而主线程则一直在执行入栈 出栈操作执行任务。

到这里我们就可以理解上文中的代码段为什么会输出 2 3 1,而不是 1 2 3 了。用图片中的知识来解释的话,就是:因为执行setTimeout()后,会立即把匿名函数放到callback queue里面等待主线程的召唤,但这个时候stack里面并不是空的,因为还有一句console.log(2)以及console.log(3)。等到执行完console.log(2)、console.log(3)后,才通过event loop把匿名函数放到stack里面,这时候它才执行,因此1会后输出。如果它执行的时候,stack为空,那它就会按时执行了。

10-04 11:09