事件循环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) 原则的有序集合,队列在尾部添加新元素,并在顶部移除元素,最新添加的元素必须排在队列的末尾。在计算机科学中,最常见的例子就是打印队列。


事件循环Event Loop--JS篇

上图中,主线程运行的时候,产生堆(heap)和栈(stack)。


JavaScript 中引用类型值的大小是不固定的,因此它们会被存储到堆内存中,由系统自动分配存储空间。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,而是操作对象的引用。


而 JavaScript 中的基础数据类型都有固定的大小,因此它们被存储到栈内存中。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。此外,栈内存还会存储对象的引用 (指针)以及函数执行时的运行空间。


栈内存和堆内存比较:


栈内存 堆内存
存储基础数据类型 存储引用数据类型
按值访问 按引用访问
存储的值大小固定 存储的值大小不定,可动态调整
由系统自动分配内存空间 程序员通过代码进行分配
主要用来执行程序 主要用来存放对象
空间小,运行效率高 空间大,但是运行效率相对较低
先进后出,后进先出 无序存储,可根据引用直接获取


3.1 执行栈

当开始执行一个方法的时候,会生成一个运行Javascript代码的环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数,以及这个作用域中定义的变量和this的指向,而当一系列方法被依次调用的时候。由于 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是执行栈。


3.2  任务队列

异步任务触发条件达成,将回调事件放到任务队列中,遵循先进先出的原则。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈是一个类似于函数调用栈的运行容器,执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行。


4.事件循环

如上面讲的,JS引擎线程只执行执行栈中的事件,执行栈中的代码执行完毕从"任务队列"中读取事件,这个过程是循环不断的,这种运行机制又称为Event Loop(事件循环)。


事件循环Event Loop--JS篇


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');


  1. 同步任务console.log('start')立即执行;

  2. setTimeout放入Event Table中,1秒后触发条件完成将回调函数放入宏任务的EventQueue中;

  3. new Promise同步代码,所以立即执行console.log('promise'),微任务then,将其放入微任务的Event Queue中;

  4. 接下来执行同步代码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(正式版本)打印结果如下(浏览器不同可能会有差异):


事件循环Event Loop--JS篇

  1. 同步任务console.log('script start')直接打印;

  2. 宏任务setTimeout,定时器线程触发条件达成,将回调事件放到任务队列中。

  3. 同步任务console.log('async1 start')直接打印;

  4. Promise.resolve同步任务,执行函数async2,打印console.log('async2'); 遇到微任务Promise.then()加入事件队列中;

  5. Promise同步任务,执行console.log('promise1'); 遇到微任务Promise.then()加入事件队列中;

  6. console.log('script end'); 

  7. 此时执行栈中任务全部完成,从任务队列中先依次取出微任务,执行 console.log('async1 end')console.log('promise2'); 

  8. 微任务执行完,执行栈再次空出,取出宏任务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篇的主要内容,如果未能解决你的问题,请参考以下文章

JS | Event Loop 事件循环

浏览器和Node不同的事件循环(Event Loop)

事件循环(event loop)

js事件循环机制(Event Loop)

事件循环(Event Loop)机制

深入理解JavaScript的事件循环(Event Loop)