0. 前言
在这篇文章中,小编总结一下JavaScript执行机制相关的几个问题,主要包括以下四个方面的内容:
- Eventloop
- 异步
- 宏任务和微任务
- promise和setTimeOut
1. EventLoop
我们知道,JavaScript语言的一大特点就是单线程,也就是说,同一个时间,只能做一件事情。但是,我们需要注意的是JavaScript是脚本语言,那么它一定是在某种宿主环境下运行的。一般来讲,JavaScript运行的宿主环境要么是浏览器,要么是NodeJS。他们都不是单线程的。
单线程意味着如果有多个需要执行,他们只能排队,需要等到前一个任务结束之后,才会执行后一个任务。这就意味着,如果前一个任务耗时很长,那么后一个任务就必须等在那里。正常来讲,如果任务是在做计算,这样做其实是没有太大问题的。但如果遇到主线程中的某个任务需要等待外部IO的响应(比如说在等待某个ajax请求的响应),而且这个响应很慢。这种情况下,后面的任务会一直等在那里,而且系统也没事情可干。这就造成了资源的浪费。
于是,JavaScript的设计者提出了这样的结构:将所有的任务分成两种,一种是同步任务,一种是异步任务。
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才会执行后一个任务
- 异步任务:不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。比如说:ajax请求,定时器等等都是异步任务
以 Node 为例,JavaScript的执行机制如下:
在进程启动时,Node便会创建一个类似于
while(true)
的循环,每执行一次循环体的过程我们称为一个Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下一个循环,如果不在有时间处理,就退出进程。
前面说了这么多,那么和 EventLoop 有什么关系呢?主线程不断的从“任务队列”中读取事件执行,直到任务队列为空,这个运行机制被称为EventLoop(事件循环)。
在这部分的最后,我们具体阐述几个相关概念:
- 任务队列:是一个事件队列,也可以理解为消息队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关的异步任务可以进入主线程执行了。
- 回调函数:会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数
2. 异步
在这一个部分,我们主要回答三个问题:
- 异步是怎么产生的?
- 当异步发生的时候,JavaScript内部发生了哪些事情?
- 当异步操作有了结果之后,如何判断任务队列中有任务要处理?
首先来看第一个问题,异步是怎么产生的。
异步最主要的产生原因就是要等待系统的I/O响应,比如说对于文件的处理,对于网络套接字的处理等。除此之外,还有非I/O产生的异步,比如说定时器(setTimeOut()
, setIntervel()
, setImmediate()
, process.nextTick()
等)
总结以下,就是执行之后不能立即产生结果,需要等待外部输入或者外部处理的操作都是异步操作。
当异步发生之后,JavaScript内部机制又进行了哪些处理呢? 事实上,从JavaScript发器调用到异步操作有结果的这个过渡过程中,有一个中间产物,叫做请求对象。
请求对象是异步操作的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及异步操作结束后的回调处理。
具体的异步操作如下图所示:
最后一个问题:当异步操作之后,如何判断任务队列有任务要处理。关于这个问题,我们需要引入观察者。每个事件循环中都有一个或多个观察者,而判断是否有时间处理的过程就是像这些观察者询问是否有要处理的事件。观察者将事件进行了分类,并且对于观察者的询问也有优先级。
3. 宏任务,微任务
在js的所有异步任务中,根据不同的任务源,又可以分为宏任务和微任务,分别存储在任务队列和微任务队列中。
- 宏任务:同步任务,setTimeOut,setInterval,I/O,UI交互事件,postMessage,MessageChannel,setImmediate
- 微任务:Promise.then,Object.observe,MutationObserver,process.nextTick
在JS中,具体的执行机制是:
- 执行一个宏任务(栈中没有就从事件列表中获取)
- 执行过程中如果遇到微任务,就把它添加到微任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当把微任务队列清空后,开始进行其他的工作。比如进行页面渲染(在Node中,会直接进入下一个宏任务的执行)
- 当JS再次接管浏览器线程之后,开始执行下一个宏任务(返回第一步执行)
4. promise 和 setTimeOut
在文章的最后,我们来解决这样一个问题:
console.log(`start`);
setTimeout(() => {
console.log(`timer1`);
})
new Promise((resolve, reject) => {
resolve(`promise1`);
}).then(data => {
console.log(data);
})
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(`timer2`);
})
resolve(`promise2`);
}).then(data => {
console.log(data)
});
setTimeout(() => {
console.log(`timer3`);
new Promise((resolve, reject) => {
resolve(`promise3`);
}).then(data => {
console.log(data)
});
});
console.log(`end`);
复制代码
上述代码的执行结果是什么?
先说结论:
start
end
promise1
promise2
timer1
timer2
timer3
promise3
复制代码
然后我们来看一下为什么是这样的结果:
在前面的内容中,我们知道,js的执行机制是先去执行同步任务,然后再去找微任务队列,清空微任务队列之后才回去检测任务队列的异步任务。显然,在上述代码中, start
和 end
都是同步任务打印出来的,它们应该先被打印。然后,接下来应该被执行的应该是微任务队列中的任务。在这段代码中,有两个promise
属于微任务的,因此,接下来执行的是这两个promise的决议。最后,才会轮得到 setTimeOut
这个宏任务。