1
众所周知,js是单线程的,说到线程,我们首先来仔细辨析一下线程和进程的知识。

一、进程与线程

阮一峰老师的一篇文章写的很好

cpu会给当前进程分配资源,进程是资源分配的最小单位,进程的资源会分配给线程使用,线程是CPU调度的最小单位

1 ——CPU就像是一个大型的工厂一样。它就像一座工厂,时刻在运行。

2 ——假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

3 ——进程好比是一个工厂的车间,它代表CPU所能处理的单个任务,任意时刻,CPU只能运行一个车间的任务,也就是一个进程,其他进程处于等待状态。

4 —— 一个车间里有着许多的工人,他们协同完成一个任务。

5 ——单个工人就好比是一个线程,一个进程可以包括多个线程。

6 —— 车间里的空间是可以供工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

7 —— 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

8 —— 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

9 —— 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

10 —— 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

归纳一下:
  • 以多进程的形式,允许多个任务同时进行。
  • 以多线程的形式,允许单个任务分成不同的部分进行运行。
  • 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

二、JavaScript是单线程

虽然说js是单线程的,但是浏览器并不是单线程的,浏览器中的许多异步行为都是由浏览器去新开一个线程去解决的,js引擎线程是浏览器的线程之一,由于js引擎线程本身是单线程的,所以我们平时说的js单线程指的就是这个了。

浏览器还包括很多其他线程,如界面渲染线程,浏览器事件触发线程,Http请求线程等。

1、证明js是单线程的

// 证明js是单线程的
function foo() {
    console.log("first");
    setTimeout(( function(){
        console.log( 'second' );
    }),5);
}
 
for (var i = 0; i < 1000000; i++) {
    foo();
}
// 执行结果会首先全部输出first,然后全部输出second;尽管中间的执行会超过5ms。

2.js为什么要设计成单线程的,

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

HTML5中增加了web worker可以去创建一个子线程,但是这个线程仍旧是完全受主线程控制,因此,js的单线程性,依旧是没有变化的。

三、任务队列

单线程就意味着,所有的任务队列都需要排队,前一个任务结束,再执行后一个任务,如果前一个任务耗时很长,那么后一个任务就不得不排着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

js语言的设计者也注意到了这个问题,这时候不管IO,挂起来,去执行等待中的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是这样就出现了同步任务异步任务了。

1、同步任务

同步任务是指主线程排队的任务,只有前一个执行完毕,后一个才能执行。

2、异步任务

不进入主线程,而是进入一个任务队列,只有任务队列,通知了主线程,某一个异步任务才会执行。

下面以AJAX请求为例,来看一下同步和异步的区别:
  • 异步AJAX:
主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“好的,主线程。我马上去发,但可能要花点儿时间呢,你可以先去忙别的。”

主线程::“谢谢,你拿到响应后告诉我一声啊。”

(接着,主线程做其他事情去了。一顿饭的时间后,它收到了响应到达的通知。)

  • 同步AJAX:
主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“......”

主线程::“喂,AJAX线程,你怎么不说话?”

AJAX线程:“......”

主线程::“喂!喂喂喂!”

AJAX线程:“......”

(一炷香的时间后)

主线程::“喂!求你说句话吧!”

AJAX线程:“主线程,不好意思,我在工作的时候不能说话。你的请求已经发完了,拿到响应数据了,给你。”

正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。异步是这篇文章关注的重点。

异步过程

  1. 所有的同步任务都再主线程上执行,形成一个执行栈
  2. 主线程之外还存在一个任务队列,一旦任务队列中的异步任务执行完毕了,就会产生一个事件。
  3. 一旦主线程上的同步任务执行完毕了,那么系统就会读取任务队列,看看有哪些事件。那些事件对应的异步任务就结束等待状态,进入执行栈,开始执行。
  4. 主线程会不断的重复上诉过程。只要是主线程空了,那么就执行任务队列中的任务。

四、事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

所谓的回调函数就是指被主线程挂起的代码,异步任务必须执行回调函数,当其产生事件,由主线程调入执行栈后就会执行这个回回调函数。

任务队列就是一个先进先出的一个数据结构,排在前面的事件,优先被主线程调用,主线程取得过程上是自动的,只有当主线程一变为空,那么任务队列的第一位就会进入主线程,那么就会执行对应的回调函数。

异步函数
A(args, callbackFn);

一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数)A
  • 回调函数`callbackFn`

它们都是在主线程上调用的,其中注册函数用来发起异步过程回调函数用来处理结果

来一个例子:

DOM点击事件
var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
    console.log(e);
});

从事件的角度来分析的话,在按钮上添加了一个鼠标点击的监听器,鼠标点击的时候出发。

从异步角度分析:

addEventListener函数就是一个发起函数,第二个回调参数就是回调函数, 事件触发的时候,表示异步任务执行完毕,就产生了事件,将其放入到消息队列中去,等待主线成的调用。

这里又出现了一个新的词汇消息队列,其实这里面放的就是任务队列执行完毕后的那些事件通知。等待着主线程的调用。接下来对其再仔细分析。

五、消息队列和事件循环(Event Loop)

未完待续~~

Meils
1.6k 声望157 粉丝

前端开发实践者