一、关于javascript
1.单线程
众所周知,JavaScript 是单线程的,所谓单线程,就是指程序只能一段一段的执行,一次只能完成一个任务,如果有多个任务就必须要排队,前面的一个任务完成了,再执行后面的任务,以此类推。
需要注意的是 JavaScript 只在一个线程上运行,不代表浏览器内核只有一个线程,事实上浏览器内部有多个线程,主线程用于 JavaScript 代码的编译和执行,其它线程都是在后台配合主线程。
JavaScript 之所以选择单线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,多线程需要面临锁、状态同步等问题,这对于一种网页脚本语言来说开销太大。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征
2.同步和异步
既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个按顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此javascript将任务分为两类:
● 同步任务:就是上面说的排队等待的形式。
● 异步任务:异步操作发生在未知或不可预测的时间,是指在执行一个任务的时候不能立即返回结果,而是在将来通过一定途径得到,后一个任务不用等前一个任务结束就执行。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
那么 JavaScript 是如何来执行异步任务的呢,就是后面要说的事件循环机制。
3.栈
栈 是一种 LIFO(Last In, First Out)的数据结构,特点即 后进先出。
类似我们吃过的薯片,薯片在包装时只能从顶部放出,拿出来的时候也只能从顶部拿出。
4.队列
队列 是一种 FIFO(First In, First Out) 的数据结构,它的特点就是 先进先出
生活中最常见的例子就是排队,排在队伍最前面的人最先被提供服务。
5.Event Table
Event Table 可以理解成一张 事件->回调函数 对应表
它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表
6.Event Queue
Event Queue 简单理解就是回调函数队列,所以它也叫Callback Queue
当Event Table中的事件被触发,事件对应的回调函数就会被 push进这个Event Queue,然后等待被执行
二、javascript事件循环

用文字来表述的话:
● 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
● 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
● 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
● 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
我们不禁会问,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
说了这么多文字,不如直接一段代码更直白:
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
上面是一段简易的ajax请求代码:
● ajax进入Event Table,注册回调函数success。
● 执行console.log(‘代码执行结束’)。
● ajax事件完成,回调函数success进入Event Queue。
● 主线程从Event Queue读取回调函数success并执行。
相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。
三、setTimeout
setTimeout是浏览器最常见的异步操作方式之一,对它第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:
setTimeout(() => {
console.log('延时3秒');
},3000)
我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
//代码1
console.log('1');
setTimeout(() => {
console.log('2')
},0);
console.log('3')
答案是不会的,事件循环必须等栈清空,才能将队列中的任务压入栈中,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
//1
//3
//2
//代码的输出顺序仍为1,3,2
定时任务可能不能按时执行
const test = () => {
let t = +new Date()
while (true) {
if (+new Date() - t >= 5000) {
break
}
}
}
setTimeout(() => {
console.log(2)
}, 2000)
test()
// 等到5秒钟后才打印出了2
因为 test 是会耗时 5 秒钟的同步任务,异步任务只能等待同步任务执行完之后才能执行,也就是说只能等 5 秒钟后才能检查的任务队列里的任务。
上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于设定的时间。
定时器最小间隔
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。
根据HTML的标准,最低是4毫秒。
四、setInterval
上面说完了setTimeout,接下来说说setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
五、Promise与process.nextTick(callback)
传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
六、宏任务和微任务
除了广义的同步任务和异步任务,我们对异步任务有更精细的定义:
● macro-task(宏任务):包括整体代码script,setTimeout,setInterval
● micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用一段代码说明:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
● 这段代码作为宏任务,进入主线程。
● 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
● 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
● 遇到console.log(),立即执行。
● 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
● ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
● 结束
事件循环,宏任务,微任务的关系如图所示:

练习:以下代码的输出结果
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
列表分析:
| 宏 | 微 | 第一轮 |
|---|---|---|
| I/O | nextTick1 | 1,7,6,8 |
| setTimeout1 | then1 | |
| setTimeout2 |
| 宏 | 微 | 第二轮 |
|---|---|---|
| setTimeout1 | nextTick1 | 2,4,3,5 |
| then2 |
| 宏 | 微 | 第三轮 |
|---|---|---|
| setTimeout2 | nextTick3 | 9,11,10,12 |
| then3 |
所以最后的输出结果是1,7,6,8,2,4,3,5,9,11,10,12
最后用一张图表示完整的事件循环执行机制





