JavaScript:事件循环机制-宏任务微任务

前面的话

本文将详细介绍javascript中的事件循环event-loop,目标是让你彻底弄懂JavaScript的执行机制。


不论是在你面试求职,或是日常开发工作中,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为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('代码全都执行完啦');

JavaScript:事件循环机制-宏任务微任务_第1张图片

依照JS是按照代码顺序执行这个理念,你可能会自信的写下输出结果:

//"开始定时器"
//"创建promise啦"
//"执行then方法咯"
//"代码全都执行完啦"

结果丢到chrome去验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?

执行结果如下:

// 创建promise啦
// 代码全都执行完啦
// 执行then方法咯
// 开始定时器

 

JavaScript:事件循环机制-宏任务微任务_第2张图片

所以为了日后的开发和面试,我们真的要彻底弄明白JavaScript的执行机制了。

1、关于JavaScript

javascript是单线程的语言,也就是说,同一个时间只能做一件事。而这个单线程的特性,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

2、线程

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

排队执行

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是在处理网络请求时这是不合适的。因为一个网络请求的资源什么时候返回是不可预知的,这种情况再排队等待就不明智了。

3、同步和异步

所以,人们将任务分成了同步任务和异步任务。当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

同步

如果在函数返回的时候,调用者就能够马上得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。

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中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。

对于同步任务来说,按顺序执行即可;但是,对于异步任务,各任务执行的时间长短不同,执行完成的时间点也不同,主线程如何调控异步任务呢?这就用到了消息队列。

3、消息队列

消息队列有很多种叫法,任务队列,或者叫事件队列,总之它是一个和异步任务相关的队列。

可以确定的是,它是队列这种先入先出的数据结构,和排队是类似的,哪个异步任务完成的早,就排在前面。不论异步操作何时开始执行,只要异步操作执行完成,就可以到消息队列中排队这样,主线程在空闲的时候,就可以从消息队列中获取消息并执行。

消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关。但是为了简单起见,可以认为:消息就是注册异步任务时添加的回调函数。

可视化描述

人们把javascript调控同步和异步任务的机制称为事件循环,首先来看事件循环机制的可视化描述

JavaScript:事件循环机制-宏任务微任务_第3张图片

 栈

函数调用形成了一个栈帧

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 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。

同步异步任务的执行机制

JavaScript:事件循环机制-宏任务微任务_第4张图片

上述流程图用文字表述是这样的:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue()。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

通过对线程同步异步消息队列的简单了解,我们对JS的执行机制有了一定的认识。根据上图的认识,我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

接下来就可以详细的讲述事件循环了。

4、事件循环

下面来详细介绍事件循环。下图中,主线程运行的时候,产生堆和栈,栈中的代码调用各种外部API,异步操作执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取消息队列,依次执行那些异步任务所对应的回调函数。

JavaScript:事件循环机制-宏任务微任务_第5张图片

详细步骤如下:

  1. 所有同步任务都在主线程上执行,形成一个执行栈.
  2. 主线程之外,还存在一个"消息队列"。只要异步操作执行完成,就到消息队列中排队。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取消息队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

循环

从代码执行顺序的角度来看,程序最开始是按代码顺序执行代码的,遇到同步任务,立刻执行。

遇到异步任务,则只是调用异步函数发起异步请求。此时,异步任务开始执行异步操作,执行完成后到消息队列中排队。

程序按照代码顺序执行完毕后,查询消息队列中是否有等待的消息。如果有,则按照次序从消息队列中把消息放到执行栈中执行。执行完毕后,再从消息队列中获取消息,再执行,不断重复。

由于主线程不断的重复获得消息、执行消息、再取消息、再执行。所以,这种机制被称为事件循环。用代码表示大概是这样:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果当前没有任何消息queue.waitForMessage 会等待同步消息到达。

事件

为什么叫事件循环?而不叫任务循环或消息循环。究其原因是消息队列中的每条消息实际上都对应着一个事件,DOM操作对应的是DOM事件,资源加载操作对应的是加载事件,而定时器操作可以看做对应一个“时间到了”的事件。

5、宏任务和微任务

简单的来说,微任务和宏任务皆为异步任务,但是微任务的优先级高于宏任务。

宏任务类型(macro-task):包括整体代码script,setTimeout,setInterval

JavaScript:事件循环机制-宏任务微任务_第6张图片

微任务类型(micro-task):

JavaScript:事件循环机制-宏任务微任务_第7张图片

 

 不同类型的任务会进入对应的Event Queue,比如setTimeoutsetInterval会进入相同的Event Queue。

JavaScript:事件循环机制-宏任务微任务_第8张图片

事件循环的顺序,决定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。(注册过程与上同,下文不再描述)
  • 接下来遇到了Promisenew Promise立即执行,打印文字,then函数分发到微任务Event Queue
  • 遇到console.log(),立即执行,打印文字。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了在微任务Event Queue里面有Promise.then方法,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。

JavaScript:事件循环机制-宏任务微任务_第9张图片

6、写在最后

js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

JavaScript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

setImmediate

微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。

最后的最后

  • javascript是一门单线程语言
  • Event Loop是javascript的执行机制

牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!

参考资料:

这一次,彻底弄懂 JavaScript 执行机制

深入理解javascript中的事件循环event-loop

 

 

 

 

 

你可能感兴趣的:(JavaScript,前端开发,前端,JavaScript,执行机制,宏任务,微任务)