本文将详细介绍javascript中的事件循环event-loop,目标是让你彻底弄懂JavaScript的执行机制。
不论是在你面试求职,或是日常开发工作中,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为javascript是一门单线程语言,所以我们可以得出结论:
那么我们以为的JS代码可能是长这样的:
let a = '我是第一';
console.log(a);
let b = '我是第二';
console.log(b);
然而实际开发中js是这样的:
setTimeout(function(){
console.log('开始定时器')
});
new Promise(function(resolve){
console.log('创建promise啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then方法咯')
});
console.log('代码全都执行完啦');
依照JS是按照代码顺序执行这个理念,你可能会自信的写下输出结果:
//"开始定时器"
//"创建promise啦"
//"执行then方法咯"
//"代码全都执行完啦"
结果丢到chrome去验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?
执行结果如下:
// 创建promise啦
// 代码全都执行完啦
// 执行then方法咯
// 开始定时器
所以为了日后的开发和面试,我们真的要彻底弄明白JavaScript的执行机制了。
javascript是单线程的语言,也就是说,同一个时间只能做一件事。而这个单线程的特性,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
排队执行
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是在处理网络请求时这是不合适的。因为一个网络请求的资源什么时候返回是不可预知的,这种情况再排队等待就不明智了。
所以,人们将任务分成了同步任务和异步任务。当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
同步
如果在函数返回的时候,调用者就能够马上得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
Math.abs(-5);
console.log('hello world');
在第一个函数返回时,就拿到了预期的返回值:-5 的绝对值;
在第二个函数返回时候,就看到了预期的效果:在控制台打印了一个字符串,所以这两个函数都是同步的。
异步
如果在函数返回的时候,调用者还不能够马上得到预期的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。一般通过传入回调函数来得到异步函数返回结果。
axios({
method: "get",
url: `url`
}).then(resp => {
console.log("请求成功", resp);
}.catch(resp => {
console.log("请求失败", resp);
});
在上面代码中我们通过axios发送网络请求,不会马上得到我们期望的结果,只有在网络请求完成后,我们才能打印出结果,所以axios函数是异步的。
正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
对于同步任务来说,按顺序执行即可;但是,对于异步任务,各任务执行的时间长短不同,执行完成的时间点也不同,主线程如何调控异步任务呢?这就用到了消息队列。
消息队列有很多种叫法,任务队列,或者叫事件队列,总之它是一个和异步任务相关的队列。
可以确定的是,它是队列这种先入先出的数据结构,和排队是类似的,哪个异步任务完成的早,就排在前面。不论异步操作何时开始执行,只要异步操作执行完成,就可以到消息队列中排队这样,主线程在空闲的时候,就可以从消息队列中获取消息并执行。
消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关。但是为了简单起见,可以认为:消息就是注册异步任务时添加的回调函数。
可视化描述
人们把javascript调控同步和异步任务的机制称为事件循环,首先来看事件循环机制的可视化描述
栈
函数调用形成了一个栈帧
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7));
当调用bar
时,创建了第一个帧 ,帧中包含了bar
的参数和局部变量。当bar
调用foo
时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了foo
的参数和局部变量。当foo
返回时,最上层的帧就被弹出栈(剩下bar
函数的调用帧 )。当bar
返回的时候,栈就空了。
堆
对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。
队列
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。
同步异步任务的执行机制
上述流程图用文字表述是这样的:
通过对线程、同步异步和消息队列的简单了解,我们对JS的执行机制有了一定的认识。根据上图的认识,我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
接下来就可以详细的讲述事件循环了。
下面来详细介绍事件循环。下图中,主线程运行的时候,产生堆和栈,栈中的代码调用各种外部API,异步操作执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取消息队列,依次执行那些异步任务所对应的回调函数。
详细步骤如下:
循环
从代码执行顺序的角度来看,程序最开始是按代码顺序执行代码的,遇到同步任务,立刻执行。
遇到异步任务,则只是调用异步函数发起异步请求。此时,异步任务开始执行异步操作,执行完成后到消息队列中排队。
程序按照代码顺序执行完毕后,查询消息队列中是否有等待的消息。如果有,则按照次序从消息队列中把消息放到执行栈中执行。执行完毕后,再从消息队列中获取消息,再执行,不断重复。
由于主线程不断的重复获得消息、执行消息、再取消息、再执行。所以,这种机制被称为事件循环。用代码表示大概是这样:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
如果当前没有任何消息queue.waitForMessage
会等待同步消息到达。
事件
为什么叫事件循环?而不叫任务循环或消息循环。究其原因是消息队列中的每条消息实际上都对应着一个事件,DOM操作对应的是DOM事件,资源加载操作对应的是加载事件,而定时器操作可以看做对应一个“时间到了”的事件。
简单的来说,微任务和宏任务皆为异步任务,但是微任务的优先级高于宏任务。
宏任务类型(macro-task):包括整体代码script,setTimeout,setInterval
微任务类型(micro-task):
不同类型的任务会进入对应的Event Queue,比如setTimeout
和setInterval
会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用本系列文章最开始的一段代码说明:
setTimeout(function(){
console.log('开始定时器')
});
new Promise(function(resolve){
console.log('创建promise啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then方法咯')
});
console.log('代码全都执行完啦');
setTimeout
,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)Promise
,new Promise
立即执行,打印文字,then
函数分发到微任务Event Queue。console.log()
,立即执行,打印文字。setTimeout
对应的回调函数,立即执行。js的异步
我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。
事件循环Event Loop
事件循环是js实现异步的一种方法,也是js的执行机制。
JavaScript的执行和运行
执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。
setImmediate
微任务和宏任务还有很多种类,比如setImmediate
等等,执行都是有共同点的,有兴趣的同学可以自行了解。
最后的最后
牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!
参考资料:
这一次,彻底弄懂 JavaScript 执行机制
深入理解javascript中的事件循环event-loop