JavaScript

JS事件循环机制

字数:2329    阅读时间:12min
阅读量:66

前言:

在快节奏的开发任务中,我们越来越依赖各种基于JS的框架,这确实能快速提高我们的开发速度,但是我们不可能只停留在对框架、语言的基础应用上;在写完越来越多的bug之后,我们深刻的体会到了解底层对一个程序员来说是多么必要的一件事;在了解和掌握各种底层原理之后,我们就可以站在更高的维度更加快速、精准、合理的解决工作学习中遇到的问题;那么我们就开始从JS的基础开始,了解JS的运行机制;接下来我们将一起整理探讨JS的事件循环机制(EnentLoop)

我们都知道JS在设计之初就是单线程语言,在遇到需要耗时的操作时我们会将需要耗时的异步操作放到一个异步队列,然后等待JS空闲再去循环执行事件队列。这种事件队列(EventQueue)+事件循环机制(EventLoop)方式,就是JS用来实现单线程异步编程的事件驱动机制(event-driven)

在理解JS的事件循环机制之前需要了解一些概念:

  • 主线程(栈):用来执行JS代码(经常说JS是单线程),先进后出
  • 任务队列(task queue):用来存放JS的异步任务,任务队列又分为宏任务和微任务,先进先出
  • 宏任务: script、setTimeout 、setInterval 、I/O(Ajax请求)、事件绑定、UI rendering
  • 微任务: Prompise的then和catch、 MutaionObserver (H5)、process.nextTick(node.js)

众所周知我们的JS是单线程运行的,但是如果有一个段代码需要很长时间才会有结果,总不能让下面代码一直等着吧,于是任务队列就出现了。我们可以把需要耗时的任务扔进任务队列,等主线程的代码运行完毕之后,再去清理任务队列的任务;(注意:上面的宏任务、微任务涉及到的setTime,Prompise等只是事件源,真正放进任务队列的是他们的回调函数)

事件循环机制流程:

  1. 处理完成主栈任务;(清空同步任务)
  2. 来到微任务队列,依次循环的把微任务压入主栈进行处理,直至清空所有微任务;(清空异步微任务)
  3. 来到宏任务队列,把当前宏任务压入主栈,执行同步代码,如果有微任务就放进微任务队列,如果有宏任务就放入宏任务队列,然后再次执行并清空微任务列表,再去到宏任务列表...如此循环,直至清空所的宏任务
事件循环图

如此便是JS的事件循环了;下面我们解和具体实例来深入理解一下它的循环过程:

打印结果

1:JS由上到下依次加载代码,首先发现 '5:宏任务1time1' 的 setTimeout, 将他放入异步队列的宏任务队列中;接着打印' 1:script主线程宏任务1 '

2:然后又发现 '8:宏任务time2' 的 setTimeout ,将他放入异步队列的宏任务队列中 ;然后执行 new Promise 发现有同步代码 '2:宏任务1promise' ,就打印 '2:宏任务1promise' 。然后发现promise的then和catch,将他们放入异步队列的微任务中

3:接着发现 '3:script主线程宏任务2' ,打印 '3:script主线程宏任务2' 。如此JS的主线程的代码全部运行完毕;

4:接着JS开始取异步队列的微任务,发现有一个 '2:宏任务1promise' 的then的回调,然后推进主栈并运行,接着打印 '4:微任务1then' 。然后主栈又运行完毕,再去微任务队列发现微任务队列也被清空;

5:接着js开始去宏任务队列里找到 '5:宏任务1time1' 的 setTimeout的回调,将他推入主栈并运行,发现 '5:宏任务1time1' ,打印 '5:宏任务1time1' ;

6:接着运行 '6:宏任务1promise' 的promise,然后打印 '6:宏任务1promise' ;再接着运行发现 '9:宏任务2time2' 的 setTimeout ,将它的回调推入宏任务队列;

7:接着发现 '6:宏任务1promise' 的promise的then,然后将它的回调推入微任务队列;至此第一个宏任务运行完毕,主栈代码运行完毕;接着JS再去微任务队列发现 '7:宏任务1time1微任务then' 的回调,并将此推入到主线程并运行,然后打印 '7:宏任务1time1微任务then' 。主栈又被清空,再去微任务队列,发现微任务队列也被清空

8:然后就进入到宏任务队列中发现 '8:宏任务time2'的 setTimeout ,将它的回调推入主栈,主栈开始运行并打印 '8:宏任务time2';然后主栈执行完,第二个宏任务结束;再去查看微任务列表发现是空的,再去宏任务队列

9:发现宏任务队列 '9:宏任务2time2' 的回调,将此推入主栈,然后运行主栈打印 '9:宏任务2time2' ,然后主栈代码运行完毕,再去微任务队列发现也被清空后,再去宏任务发现也被清空,至此整个js运行完毕

接下来再看一个例子:

我们来具体分析一下这两个for循环为什么能打印处不一样的东西:

  • 首先第一个for循环:当js执行到for循环时发现i变量是由var声明的,就将i的变量提升到最外面然后执行同步代码for循环,每循环依次发现一个定时器,不停的往宏任务里面丢,然后for循环结束js代码结束此时i=10,再去宏任务调用打印自然打印的都是10;
  • 第二个for循环:js执行到i的时候发现是let声明的,就在当前for循环(for的大括号内)创建一个作用域,当前作用域下的i就是当前i的值,然后发现定时器,放入宏任务,循环结束,每个定时器放入宏任务的时候他的执行上下文环境也跟着进去,所以当每一个宏任务打印的时候都能拿到当前作用域下当前值,也就能打印0~9的值了

野生小园猿
励志做一只遨游在知识海洋里的小白鲨
查看“野生小园猿”的所有文章 →

相关推荐