一、单线程

  • JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是唯一的,所有的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数

  • JS最初被设计使用在浏览器上,作为浏览器上的脚本语言,需要与用户的操作互动以及操作DOM, 如果多线程的话,需要关注各个线程之间状态的同步问题。

  • 单线程有什么问题:

    • 我们常见的异步操作(定时器延迟、网络请求、网页事件等), 事件触发之后并不能立即得到结果,按照之前的运行模式,浏览器就会阻塞其他操作,等待相应结果,表现在页面中就是页面卡死,这是一个优秀的应用所不能允许发生的。
  • 什么是同步异步

    • 一般操作可以分为两个步骤,发起调用、得到结果;
    • 发起调用,立马可以得到结果的是为同步;
    • 发起调用,无法立即得到结果,需要额外操作才能得到结果的是为异步。

所以这就需要一种解决方案:

  • 问题的本质是js引擎的单线程工作模式,只专注于一件事情, 必须【执行至完成】
  • 而产生问题的那些操作往往不能直接得到 结果,必须经过额外操作才能得到结果 【异步问题】

一种思路:

  • 可以把这些异步操作分发给其他模块,得到处理结果之后再把回调函数一块放入主线程执行。
  • 这就是 事件循环(Event Loop)的主体思路。
  • event loop 只是解决异步问题的一种思路 其他的思路还有:轮询、事件

二、web API模块及js执行线程

  • 异步操作可以交给JS引擎之外的其他模块处理, 在浏览器中其他模块就是Web API模块
  • 所以现在JS引擎中,执行栈遇到同步函数调用,直接执行得到结果后,弹出栈,继续下一个函数调用
  • 遇到异步函数调用,将函数分发给Web API模块,然后该异步函数弹出栈,继续下一个函数调用,不会产生阻塞问题
  • web api模块内就有多个线程,每个异步操作处理模块都对应一个线程,http请求线程、事件处理线程、定时器处理线程等
  • js执行的过程中,会有4个线程参与执行,但是只有js引擎线程执行脚本文件其他三个线程只是协助,不参与代码的解析与执行。
    • JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)。
    • 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件队列,等待JS引擎线程执行。
    • 定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待JS引擎线程执行。
  • 注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。
    • HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。
  • 注:浏览器对同一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。

总结:永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。

现在存在一个问题:异步操作分发给Web API模块处理之后,不能说不管了,主线程还是需要知道结果做后续操作的,Web API得到结果之后怎么通知主线程呢?

三、回调队列(callback queue)

  • 回调队列, 也叫事件队列、消息队列。
  • 这个模块就是用来帮助Web API模块处理异步操作的。
  • 在Web API模块中,异步操作在相应的线程中处理完成得到结果之后,会把结果注入异步回调函数的参数中,并且把回调函数推入回调队列中
  • 但是,只推到回调队列里也不是个事儿,因为所有的JS执行都发生在主线程调用栈里面。这些异步操作拿到结果之后,带着回调函数推入了回调队列,需要在适当的时机进入主线程调用栈执行。
  • 那么,谁知道什么时候是合适的时机呢?
  • Event Loop 知道。

四、事件循环机制 Event Loop

  • Event Loop 不停地检查主线程调用栈和回调队列,当发现主线程空闲的时候,就把回调队列里第一个任务推入主线程执行。 以此不停地循环。

  • 至此,一个异步操作,兜兜转转最终拿到了结果,成功执行并且没有阻塞其他的操作。

  • 明确一些概念:

    • 宏任务macro-task | task:按执行顺序分为同步任务和异步任务
    • 同步任务:在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)。
1
2
console.log('script start'); 
console.log('script end');
    • 异步任务:不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件,setTimeout等。
1
setTimeout(function() {console.log('setTimeout');}, 0);

五、深入事件循环

事件循环可以理解成由三部分:

  • 主线程执行栈
  • 异步任务等待触发
  • 回调队列

任务队列(task queue)就是以队列的数据结构对事件任务进行管理,特点是先进先出,后进后出。(其实就是回调队列)

mark

JS引擎主线程执行过程中:

  • 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈
  • 当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
  • 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中,等待主线程读取执行。
  • 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
  • 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程。
1
2
3
4
5
6
7
console.log('script start');

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

console.log('script end');
  • JS引擎主线程按代码顺序执行,当执行到console.log(‘script start’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行。
  • 执行到setTimeout(function() { console.log(‘setTimeout’); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行。
  • JS引擎主线程执行到console.log(‘script end’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end。
  • JS引擎主线程上的任务执行完毕(输出script start和script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推进主线程中,按任务队列顺序执行,最终输出setTimeout,所以输出的结果顺序为script start script end setTimeout。
  • 以上便是JS引擎执行宏任务的整个过程。

5.1、微任务

微任务micro-task:ES6和node环境中出现的一个任务类型,如果不考虑es6和node环境的话,我们只需要理解宏任务事件循环的执行过程就已经足够了,但是到了es6和node环境,我们就需要理解微任务的执行顺序了。微任务(micro-task)的API主要有:Promise, process.nextTick

1
Promise.resolve().then(function() { console.log('promise1') })

加入了微任务后,新的Event Loop变成以下这样:

  • 检查宏任务中同步任务,有同步任务执行。
  • 检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件(异步任务),推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务。
  • 执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('script start');

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

Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

console.log('script end');

执行过程如下:

  • 代码块通过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log(‘script start’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行。

  • JS引擎主线程执行到setTimeout(function() { console.log(‘setTimeout’); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行。

  • JS引擎主线程执行到Promise.resolve().then(function() { console.log(‘promise1’); }).then(function() { console.log(‘promise2’); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行。

  • JS引擎主线程执行到console.log(‘script end’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end。

  • 主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么立刻执行,输出promise1和promise2

  • 微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出setTimeout

  • 输出结果:

1
2
3
4
5
6
7
script 
start
script
end
promise1
promise2
setTimeout