事件循环Event Loop--JS篇
Posted 南山zwl
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事件循环Event Loop--JS篇相关的知识,希望对你有一定的参考价值。
Event Loop,基本是面试必考题,理解它有助于帮助我们理解JS的运行机制。首先从JS单线程说起。
1. 为什么javascript是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
2.任务队列
由于是单线程,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是如果一个任务耗时很久(比如ajax请求),那后边的任务一直等着会大大影响性能,所以任务又分为两种:同步任务和异步任务。
同步任务:在主线程上只有等前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
3.执行栈和任务队列
首先需要了解数据结构中的几个概念(关于数据结构的问题,可以去查资料,我也在看这部分,暂时就不介绍了)。
堆 (heap): 堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆是一棵完全二叉树。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
栈 (stack): 栈是遵循后进先出 (LIFO) 原则的有序集合,新添加或待删除的元素都保存在同一端,称为栈顶,另一端叫做栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。栈在编程语言的编译器和内存中存储基本数据类型和对象的指针、方法调用等.
队列 (queue): 队列是遵循先进先出 (FIFO) 原则的有序集合,队列在尾部添加新元素,并在顶部移除元素,最新添加的元素必须排在队列的末尾。在计算机科学中,最常见的例子就是打印队列。
上图中,主线程运行的时候,产生堆(heap)和栈(stack)。
JavaScript 中引用类型值的大小是不固定的,因此它们会被存储到堆内存中,由系统自动分配存储空间。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,而是操作对象的引用。
而 JavaScript 中的基础数据类型都有固定的大小,因此它们被存储到栈内存中。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。此外,栈内存还会存储对象的引用 (指针)以及函数执行时的运行空间。
栈内存和堆内存比较:
栈内存 | 堆内存 |
存储基础数据类型 | 存储引用数据类型 |
按值访问 | 按引用访问 |
存储的值大小固定 | 存储的值大小不定,可动态调整 |
由系统自动分配内存空间 | 由程序员通过代码进行分配 |
主要用来执行程序 | 主要用来存放对象 |
空间小,运行效率高 | 空间大,但是运行效率相对较低 |
先进后出,后进先出 | 无序存储,可根据引用直接获取 |
3.1 执行栈
当开始执行一个方法的时候,会生成一个运行Javascript代码的环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数,以及这个作用域中定义的变量和this的指向,而当一系列方法被依次调用的时候。由于 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是执行栈。
3.2 任务队列
异步任务触发条件达成,将回调事件放到任务队列中,遵循先进先出的原则。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈是一个类似于函数调用栈的运行容器,执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行。
4.事件循环
如上面讲的,JS引擎线程只执行执行栈中的事件,执行栈中的代码执行完毕从"任务队列"中读取事件,这个过程是循环不断的,这种运行机制又称为Event Loop(事件循环)。
5.宏任务、微任务
事件循环中有宏任务、微任务之分。
宏任务:执行栈中的任务就是宏任务,常见的包括:主代码块(script),setTimeout,setInterval等。
微任务:微任务可以理解成在当前 宏任务执行后立即执行的任务。也就是说,当 宏任务执行完,会在渲染前,将执行期间所产生的所有 微任务都执行完。常见的有:Promise.then,process.nextTick(node中)。
console.log('start');
setTimeout(function() {
console.log('setTimeout');
},1000)
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('end');
同步任务console.log('start')立即执行;
setTimeout放入Event Table中,1秒后触发条件完成将回调函数放入宏任务的EventQueue中;
new Promise同步代码,所以立即执行console.log('promise'),微任务then,将其放入微任务的Event Queue中;
接下来执行同步代码console.log('end');
此时主线程的宏任务,已经全部执行完毕,接下来要执行微任务,因此会执行Promise.then,到此,第一轮事件循环执行完毕。第二轮事件循环开始,先执行宏任务,即setTimeout的回调函数,然后查找是否有微任务;没有,时间循环结束。
到此做个总结,事件循环,先执行宏任务,其中同步任务立即执行,异步任务,加载到对应的的Event Queue中(setTimeout等加入宏任务的Event Queue,Promise.then加入微任务的Event Queue),所有同步宏任务执行完毕后,如果发现微任务的Event Queue中有未执行的任务,会先执行其中的任务,这样算是完成了一次事件循环。接下来查看宏任务的Event Queue中是否有未执行的任务,有的话,就开始第二轮事件循环,依此类推。
6.async/await
async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
await命令后面是一个Promise对象,返回该对象的结果。如果不是Promise对象,就直接返回对应的值。
经典面试题:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
用Promise替换后:
function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
});
}
function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('settimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
在mac Chrome版本 80.0.3987.122(正式版本)打印结果如下(浏览器不同可能会有差异):
同步任务console.log('script start')直接打印;
宏任务setTimeout,定时器线程触发条件达成,将回调事件放到任务队列中。
同步任务console.log('async1 start')直接打印;
Promise.resolve同步任务,执行函数async2,打印console.log('async2'); 遇到微任务Promise.then()加入事件队列中;
Promise同步任务,执行console.log('promise1'); 遇到微任务Promise.then()加入事件队列中;
执行console.log('script end');
此时执行栈中任务全部完成,从任务队列中先依次取出微任务,执行 console.log('async1 end')和console.log('promise2');
微任务执行完,执行栈再次空出,取出宏任务setTimeout;
注:此篇只记录了JS的事件循环,关于事件循环其实涉及到很多内容,包括线程与进程,浏览器内核,数据结构等以及NodeJS,等我继续学习完这些内容,再进行完善。
--END--
未完待续......
最后,祝愿大家身体健康,常洗手,多通风。
欢迎关注GitHub:github.com/wlzhangYes
参考文章:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://juejin.im/post/5cbc0a9cf265da03b11f3505#heading-5
都看到这里了,给个“在看”再走呗~
以上是关于事件循环Event Loop--JS篇的主要内容,如果未能解决你的问题,请参考以下文章