3、js运行时
我们经常使用的一些API,并
不是js引擎中提供的
,例如setTimeout。它们其实
是在浏览器中提供的
,也就是运行时提供的,因此,实际上除了JavaScript引擎以外,还有其他的组件。其中有个组件就是由浏览器提供的,叫
Web APIs
,像DOM,AJAX,setTimeout等等。
然后还有就是非常受欢迎的事件循环和回调队列,运行时负责给引擎线程发送消息,只负责生产消息,不负责取消息。
4、消息队列和事件循环
主线程在执行过程中遇到了异步任务,就发起函数或者称为
注册函数
,通过event loop线程通知相应的工作线程(如ajax,dom,setTimout等),同时主线程继续向后执行,不会等待。等到工作线程完成了任务,eventloop线程会将消息添加到消息队列中,如果此时主线程上调用栈为空就执行消息队列中排在最前面的消息,依次执行。新的消息进入队列的时候,会自动排在队列的尾端。
单线程意味着js任务需要排队,如果前一个任务出现大量的耗时操作,后面的任务得不到执行,任务的积累会导致页面的“假死”。这也是js编程一直在强调需要回避的“坑”。
主线程会循环上述步骤,事件循环就是主线程重复从消息队列中取消息、执行的过程。
需要注意的是 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
因此页面渲染都是在js引擎主线程调用栈为空时进行的。
其实
事件循环
机制和
消息队列
的维护是由
事件触发线程
控制的。
事件触发线程
同样是浏览器渲染引擎提供的,它会维护一个
消息队列
。
JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由
事件触发线程
将异步对应的
回调函数
加入到消息队列中,消息队列中的回调函数等待被执行。
同时,JS引擎线程会维护一个
执行栈
,同步代码会依次加入执行栈然后执行,结束会退出执行栈。如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
5、执行顺序
了解了事件循环和消息队列之后,接下来就是弄清楚当同步任务和异步任务都存在时,代码执行的顺序究竟是怎么样的。
console.log("a");
setTimeout(function(){
console.log("b")},0
console.log("c");
大部分人都知道执行顺序是a,c,b。setTimeout在主线程执行时被添加到了消息队列中,等待主线程调用栈为空时,再从消息队列中取出执行。因此setTimeout中的延时时间并非确切的执行时间,实际上应该理解为添加到消息队列中的延迟时间。以上述代码为例,如果console.log("c")处是一个计算量很大的任务,或者消息队列中已经存在了若干个等待处理的消息。setTimeout都将延迟都将大于设置的延迟时间。
以上的内容在ES6之前就基本覆盖了执行顺序的问题,但是在ES6引入了promise后,产生了一个新的名词”微任务(microtask)“。微任务的执行顺序与之前我们所说的任务(我们可以称之为”宏任务“)是不同的。
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
console.log('script end')
输出的结果是:script start - script end - promise1 - promise2 - timer over,你答对了吗?我猜这里让你困惑的一定是为什么promise1和promise2在timer over之前输出了。下面我们来解释一下微任务这个概念。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(H5新特性)
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
new
Promise(function(resolve) {
console.log(
'
promise1
'
);
for
(
var
i =
0
; i <
1000
; i++
) {
i
==
99
&&
resolve();
console.log(
'
promise2
'
);
}).then(function() {
console.log(
'
then1
'
);
console.log(
'
global1
'
);
执行结果为:promise1 - promise2 - global1 - then1 - timeout1,分析一下代码,首先程序开始执行,遇到setTimeout时将它添加到消息队列,等待后续处理,遇到Promise时会创建微任务(.then()里面的回调),
注意此时new promise构造函数中的代码还是同步执行的,只有.then中的回调会被添加到微任务队列
。因此会连续输出promise1和promise2。继续执行到console.log('global1')输出global1,到此调用栈中已经为空。
此时微任务队列里有一个任务.then,宏任务队列里也有一个任务setTimout。
microtask必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。也就是说,
在某一个宏任务执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有微任务都执行完毕(在渲染前)
。因此会执行.then输出then1,然后进行下一轮事件循环,取出任务队列中的setTimeout输出timeout1。
总结一下执行机制:
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)