JavaScript的执行机制、事件循环、宏任务和微任务

1,407 阅读5分钟

(1)JavaScript的执行机制

1、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

2、主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

3、一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",如果有有执行任务,则进入执行栈,开始执行。

4、主线程不断重复上面的三步,此过程也就是常说的Event Loop(事件循环)。

(2)名词解释

1.执行栈

当我们调用一个方法的时候,js会生成一个与这个方法相对应的执行环境,也叫执行上下文,这个执行环境存在着这个方法的私有作用域、参数、this对象等等。因为js是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,js会先解析这些方法,把其中的同步任务按照执行顺序排队到一个地方,这个地方叫做执行栈。

2.主线程

JavaScript是单线程的,那么这个单线程就成为主线程。而事件循环在主线程执行完执行栈代码后,才执行的。所以主线程代码执行时间过长,会阻塞事件循环的执行。只有当执行栈为空的时候(同步代码执行完毕),才会执行事件循环来观察有哪些事件回调需要执行,当事件循环检测到任务队列有事件就读取出回调放到执行栈由主线程执行。

3.任务队列

任务队列也有时称叫消息队列、回调队列。

异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclick, setTimeout,ajax处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,webcore包含下图中的3种 webAPI,分别是DOM Binding、network、timer模块。

DOM Binding 模块处理一些DOM绑定事件,如onclick事件触发时,回调函数会立即被webcore添加到任务队列中。

network 模块处理Ajax请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。

timer 模块会对setTimeout等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

(3)事件循环

JavaScript整体执行过程:

主线程运行的时候会生成堆(heap)和栈(stack);

js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;

当程序调用外部的API时,比如ajax、setTimeout等,会将此类异步任务挂起,继续执行执行栈中的任务,等异步任务返回结果后,再按照执行顺序排列到任务队列中;

主线程先将执行栈中的同步任务清空,然后检查任务队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到任务队列中。

主线程每次将执行栈清空后,就去任务队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的... ...,这个过程被称为“Event Loop 事件循环”。

image.png

(4)宏任务和微任务

异步任务的执行,就涉及到了宏任务和微任务。

所有的任务在主线程执行,会形成一个执行栈,执行栈会区分出宏任务和微任务,并把任务放在各自的任务队列中。

宏任务一般包括整体SCRIPT代码块,seiTimeout,setInterval。

微任务:promise的then方法,process,nextTick。

所有的异步任务都会被分为宏任务和微任务。宏任务队列一次只会存放一个宏任务,当宏任务队列的任务执行完后,会执行所有的微任务。所有微任务执行完后,会进入下一个事件循环。

宏任务队列会进入下一个宏任务,并执行这个宏任务。

一个宏任务执行完后,执行所有的微任务,所有的微任务执行完后,再次开始执行下一个进入宏任务队列的宏任务。这个过程就是一次事件循环。

所有任务的执行就形成整个的事件循环。

  console.log('1');
  setTimeout(function() {
  console.log('2');
  new Promise(function(resolve) {
  console.log('3');
  resolve();
  }).then(function() {
  console.log('4')
  })
  })
  console.log('5');
  setTimeout(function() {
  console.log('6');
  new Promise(function(resolve) {
  console.log('7');
  resolve();
  }).then(function() {
  console.log('8')
 })
 })
 console.log('9');

上面代码执行过程:

  • 第一轮事件循环
    • 整体script代码(同步代码)作为第一个宏任务进入主线程,遇到console.log,输出1
    • 遇到setTimeout,其回调函数被放到宏任务队列中,暂记为setTmineout1
    • 遇到console.log,输出5
    • 遇到setTimeout,其回调函数被放到宏任务队列中,暂记为setTmineout2
    • 遇到console.log,输出9
    • 一个宏任务执行结束,去微任务队列查找是否有待执行的任务,没有,结束
    • 第一轮循环结束,输出:1 5 9
  • 第二轮事件循环
    • 从宏任务队列中取出一个任务,即setTmineout1,开始执行
    • 遇到console.log,输出2
    • 遇到Promise,创建Promise,输出了3,同时把Promise.then回调函数放到微任务队列
    • 一个宏任务执行结束,去微任务队列查找是否有待执行的任务, 发现有微任务,全部取出放到执行栈执行
    • 执行微任务,此时就一个微任务,console.log,输出4
    • 微任务执行结束
    • 第二轮循环结束,输出:2 3 4
  • 第三轮事件循环与第二轮一样,输出:6 7 8
  • 事件循环发现所有任务都已经处理完毕,此时程序执行结束
  • 全部的输出:1 5 9 2 3 4 6 7 8,可复制代码到chrome浏览器控制台中运行校验结果