实例详解JS中的事件循环机制

目录
  • 一、前言
  • 二、宏、微任务
  • 三、Tick 执行顺序
  • 四、案例详解
    • 1.掺杂setTimeout
    • 2.掺杂微任务,此处主要是Promise.then
    • 3.掺杂async/await

一、前言

之前我们把react相关钩子函数大致介绍了一遍,这一系列完结之后我莫名感到空虚,不知道接下来应该更新有关哪方面的文章。最近想了想,打算先回归一遍JS基础,把一些比较重要的基础知识点回顾一下,然后继续撸框架(可能是源码、也可能补全下全家桶)。不积跬步无以至千里,万丈高楼咱们先从JS的事件循环机制开始吧,废话不多说,开搞开搞!

在JS中,我们所有的任务可以分为同步任务和异步任务。那么什么是同步任务?什么又是异步任务呢?

同步任务:是在主线程执行栈上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;比如:console.log、赋值语句等。

异步任务:不进入主线程,是进入任务队列的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。比如:ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。

我们执行一段代码时,在我们主线程的执行栈执行过程中,如果遇到同步任务会立即执行,如果遇到异步任务会暂时挂起,将此异步任务推入任务队列中(队列的执行机制遵循先进先出)。当主线程执行栈里的同步任务执行完毕后,js执行引擎会去任务队列中读取挂起的异步任务并将其推入到执行栈中执行。这个不断重复的过程(执行栈执行--->判断同异步--->同步执行/异步挂起推入事件对列--->栈空后取事件队列里任务并推入执行栈执行--->继续判断同异步--->.......)就是本文所要介绍的事件循环。

二、宏、微任务

我们每进行一次事件循环的操作被称之为tick,在介绍一次 tick 的执行步骤之前,我们需要补充两个概念:宏任务、微任务。

宏任务和微任务严格来说是ES6之后才有的概念(原因在于ES6提出了Promise这个概念);在Es6之后我们把JS的任务更细分成了宏任务和微任务。

其中,宏任务主要包括:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、requestAnimationFrame(帧动画)、MessageChannel、setImmediate(Node.js环境);

微任务主要包括:Promise.then、MutaionObserver、process.nextTick(Node.js环境);

好了,了解了宏微任务的概念之后我们就来掰扯掰扯每次tick的执行顺序吧。首先看下图:

三、Tick 执行顺序

1、首先执行一个宏任务(栈中没有就从事件队列中获取);

2、执行过程中如果遇到微任务,就将它添加到微任务的任务队列中、如果有宏任务的话推到相应的事件队列中去;

3、宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);

4、当前宏任务执行完毕,开始进行渲染;5、开始下一个宏任务(从事件队列中获取)开启下一次的tick;

需要注意的是:宏任务执行过程中如果宏任务中又添加了一个新的宏任务到任务队列中。 这个新的宏任务会等到下一次事件循环再执行;而微任务则不同,微任务执行过程中如果又添加了新的微任务,则新的微任务也会在本次微任务执行过程中被执行,直到微任务队列为空。每次宏任务执行完在开启下一次宏任务时会把微任务队列中所有的微任务执行完毕!

四、案例详解

概念性的东西说完了,下面就来找些demo练练手吧!

1.掺杂setTimeout

console.log('开始');

setTimeout(()=>{
    console.log('同级的定时器');
      setTimeout(() => {
          console.log('内层的定时器');
      }, 0);
},0)

console.log('结束');

输出结果为

开始 -> 结束 -> 同级的定时器 ->内层的定时器

解释上述代码:

  • 整体代码作为一个宏任务进入主线程执行栈中;
  • 遇到console.log('开始'),控制台输出 开始;
  • 遇到有一个宏任务setTimeout,JS引擎将之挂起,并推入任务队列;
  • 遇到console.log('结束'),控制台输出 结束;本次宏任务执行完毕,发现本次并无微任务,GUI进行render渲染完毕开启下一次宏任务执行,本次tick结束。
  • JS引擎从任务队列拿出第一个setTimeout宏任务,将至推入主线程执行栈, 开始进行第二个宏任务;
  • 执行setTimeout回调,遇到 console.log('同级的定时器'),控制台输出 同级的定时器;
  • 遇到第二个setTimeout ,这是个本次宏任务产生的新的宏任务,将此宏任务挂起,并推入任务队列;
  • 同样此时发现没有微任务,则GUI接管开始进行渲染,渲染完毕又开启下一次宏任务,tick结束;
  • JS引擎又从任务队列拿出第二个setTimeout宏任务,将之推入主线程执行栈, 开始进行第三个宏任务;
  • 执行第二个setTimeout回调,遇到 console.log('内层的定时器'),控制台输出 内层的定时器;
  • 本次宏任务执行完毕发现没有微任务,结束。

2.掺杂微任务,此处主要是Promise.then

console.log('script start');

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
        console.log('这是微任务');
    })
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

输出结果为:

script start -> promise1 -> script end -> then1 -> 000 -> timeout1 -> 这是微任务 -> timeout2

解释上述代码:

  • 整体script作为一个宏任务进入主线程执行栈;
  • 遇到 console.log('script start') 输出 script start ;
  • 遇到setTimeout作为新的一个宏任务连同其回调内容一同推入任务队列 ;
  • 遇到和script start 同级的new Promise 进行执行,此处需注意:Promise内容是同步任务,它的.then才是微任务会被推入微任务队列。所有此处JS引擎的处理逻辑是:遇到 console.log('promise1') 输出 promise1 ,遇到resolve() 会将 Promise的.then函数推入微任务队列(注意,我们常说微任务时宏任务的小尾巴,指的是本次宏任务产生的微任务都会在本次宏任务执行完之后进行执行清空。);遇到resolve下面的setTimeout这是个新的宏任务,会被挂起并推入任务队列。
  • 继续顺序执行,执行到 console.log('script end') ,输出script end;此时第一个宏任务执行完毕,JS引擎开始清理小尾巴(执行并清空微任务队列)。
  • 此时由本次执行宏任务的过程中产生了 .then(function() { console.log('then1')}) 这个微任务,JS引擎会将此任务内的回调推入执行栈进行执行,输出 then1;
  • 微任务队列为空,开启下一个宏任务,第一轮tick结束;
  • JS引擎从任务队列中拿script start下面那个setTimeout宏任务将回调推入主线程执行栈中进行执行;
  • 遇到了Promise,执行其内容:遇到 console.log('000') 输出 000;
  • 执行 resolve() 将.then函数推入微任务队列(是此次宏任务的小尾巴);
  • 继续执行,遇到 console.log('timeout1') 输出 timeout1;本次宏任务执行完毕;
  • 宏任务执行完毕后紧接着处理小尾巴: .then(res=>{ console.log('这是微任务'); }) 输出 这是微任务;
  • 微任务队列清空后,继续开启下一个宏任务,第二轮tick结束;
  • 将任务队列中的 setTimeout(() => console.log('timeout2'), 10); 回调推入执行栈中执行,输出 timeout2 ; 无微任务,第三轮tick结束,任务队列也为空。

好了,相信经过这两个例子,小伙伴们对事件循环有了初步的认识。接下来我们再顽皮一下:对上面这个demo做一丢丢微调

微调一 : 其他地方不变,then里塞定时器

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
         setTimeout(()=>{
         console.log('这次的执行顺序呢?') -----> 如果这里再塞个定时器呢?执行顺序是什么?
        },10)
        console.log('这是微任务');
    })
  console.log('timeout1');
}, 10);

微调二:其他地方不变,对Promise进行链式调用

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
}).then(()=>{
    console.log('then2')
}).then(()=>{
    console.log('then3')
})

此Promise进行链式调用,其他地方不动,此时的执行顺序是什么?

提示:在一次tick结束时,此tick内微任务队列中的微任务一定会执行完并清空,如果在执行过程中又产生了微任务,那么同样会在此tick过程中执行完毕;而宏任务的执行则可以看成是下一次tick的开始。

3.掺杂async/await

在进行demo解析之前,我们需要补充一下async/await的相关知识点。

async

async相当于隐式返回Promise:当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象,async的函数会在这里帮我们隐式使用Promise.resolve();

下面看个小demo来理解下async函数是怎么隐式转换的:

async function test() {
    console.log('这是async函数')
    return '测试隐式转换'
}

上面这个async就相当于如下代码:

function test(){
    return new Promise(function(resolve) {
      console.log('这是async函数')
       resolve('测试隐式转换')
   })
}

await

await表示等待,是右侧表达式的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且await只能在带有async的内部使用;使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的 代码 当外部代码执行完毕,再回到该函数内部执行await后面剩余的代码

好了,补充完前置知识我们来做个demo助助兴:

掺杂async/await的事件循环

async function async2() {
    console.log('async2');
}

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

输出顺序为

script start --> async1 start --> async2 --> promise1 --> script end --> async1 end --> promise2 --> setTimeout

首先为方便理解我们先将async函数转为return Promise的那种形式:

①:
async function async2() {
    console.log('async2');
}
转换后如下:
function async2() {
    return  new Promise(resolve=>{
       console.log('async2');
    })
}

②:
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
转换后如下:
function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码
    console.log('async1 end');
  })
}

所以,最后我们包含async函数的代码块就相当于如下代码:

function async2() {
    return  new Promise(resolve=>{
       console.log('async2');
    })
}

function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log('async1 end')的执行
    console.log('async1 end');
  })
}
=============上面为声明部分===========
console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log(async1end)的执行;这里相当于awaitasync2()

    console.log('async1 end');
  })
}

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

经过一系列骚操作之后,我们终于可以来分析这个代码块的执行顺序了,废话不多说,开冲。

解释上述代码:

  • 首先整体代码作为第一个宏任务进入主线程执行栈;
  • 首先顺序执行,遇到了async2、async1 函数的声明,不进行任何输出;
  • 执行到console.log('script start') 输出 script start ;
  • 继续执行,遇到setTimeout宏任务,挂起并推入任务队列;
  • 接着执行Promise内容部分,遇到console.log('async1 start'),输出async1 start ;
  • 这一步重点来了,遇到了await,这该怎么办呢?别急,咱们再来看看使用await会发生什么:使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的 代码 当外部代码执行完毕,再回到该函数内部执行await后面剩余的代码
  • 好了,下面开始解释await async2():由于是是从右往左执行,所以我们首先执行了async2()输出了一个Promise,我们执行了Promise的内容输出了async2;async2执行完了之后,遇到await,完全不出意外,后面的代码被阻塞;我们去执行外面的代码;
  • 因为console.log('async1 end')被await阻塞掉了,我们先执行外面的代码:执行了外面Promise的内容,遇到了resolve(),将.then函数推入微任务队列;然后执行console.log('promise1'),输出 promise1;
  • 最后执行到console.log('script end'),输出 script end;
  • 到此,我们外层的代码就执行完毕,现在想想好像少了什么?往前一看,我们console.log('async1 end')还在等待,此时,JS引擎执行log输出 async1 end 。
  • 由此,我们本次的宏任务就执行完毕,下面看看是否有微任务,JS引擎去微任务队列一看,好家伙,还藏着一个 then(function () {console.log('promise2');}); 把此任务回调推到执行栈中执行,输出 promise2;
  • 此次tick执行结束,开启下一个宏任务;
  • 从任务队列拿setTimeout这个宏任务,塞入执行栈执行,打印输出setTimeout,本次无微任务,结束tick;
  • 循环结束;

到此这篇关于实例详解JS中的事件循环机制的文章就介绍到这了,更多相关JS事件循环机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 实例分析js事件循环机制

    本文通过实例给大家详细分析了JS中事件循环机制的原理和用法,以下是全部内容: var start = new Date() setTimeout(function () { var end = new Date console.log('Time elapsed:', end - start, 'ms') }, 500) while (new Date() - start < 1000) { } 有其他语言能完成预期的功能吗?Java, 在Java.util.Timer中,对于定时任务的解决方案

  • 详解JS浏览器事件循环机制

    先来明白些概念性内容. 进程.线程 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的. 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的. 浏览器内核 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种. 浏览器内核有多种线程在工作. GUI 渲染线程: 负责渲染页面,解析 HTML,C

  • JavaScript 关于事件循环机制的刨析

    目录 前言: 一.事件循环和任务队列产生的原因: 二.事件循环机制: 三.任务队列: 3.1 任务队列的类型: 3.2 两者区别: 3.3 更细致的事件循环过程 四.强大的异步专家 process.nextTick() 4.1 process.nextTick()在何时调用? 前言: 这次主要整理一下自己对 Js事件循环机制,同步,异步任务,宏任务,微任务的理解,大概率暂时还有些偏差或者错误.如果有,十分欢迎各位纠正我的错误! 一.事件循环和任务队列产生的原因: 首先,JS是单线程,这样设计也是

  • 简单聊聊JavaScript的事件循环机制

    目录 前言 概念 举个栗子 TIP 再次举个栗子 总结 前言 JavaScript是一门单线程的弱类型语言,但是我们在开发中,经常会遇到一些需要异步或者等待的处理操作. 类似ajax,亦或者ES6中新增的promise操作用于处理一些回调函数等. 概念 在JavaScript代码执行过程中,可以分为同步队列和异步队列. 同步任务类似我们常说的立即执行函数,不需要等待可以直接进行,可以直接进入到主线程中去执行,类似正常的函数调用等. 异步队列则是异步执行函数,类似ajax请求,我们在发起的过程中,

  • 一文详解JS中的事件循环机制

    目录 前言 1.JavaScript是单线程的 2.同步和异步 3.事件循环 前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制. 1.JavaScript是单线程的 JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事.在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行.在

  • 详解JavaScript事件循环机制

    众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心.可看HTML规范中的这段话: To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There a

  • 关于js的事件循环机制剖析

    前言 众所周知, JavaScript是单线程这一核心,可是浏览器又能很好的处理异步请求,那么到底是为什么呢?其中的原理与事件循环机制大有关系. 在探索事件循环之前,我们得先了解浏览器执行线程~~ 浏览器的渲染进程是多线程的,浏览器每一个tab标签都代表一个独立的进程,其中浏览器内核属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等.其包含的线程有以下几种 GUI 渲染线程:负责渲染页面,解析 HTML,CSS 构成 DOM 树: JS 引擎线程:解释执行代码.用户输入和网络请求:

  • 实例详解JS中的事件循环机制

    目录 一.前言 二.宏.微任务 三.Tick 执行顺序 四.案例详解 1.掺杂setTimeout 2.掺杂微任务,此处主要是Promise.then 3.掺杂async/await 一.前言 之前我们把react相关钩子函数大致介绍了一遍,这一系列完结之后我莫名感到空虚,不知道接下来应该更新有关哪方面的文章.最近想了想,打算先回归一遍JS基础,把一些比较重要的基础知识点回顾一下,然后继续撸框架(可能是源码.也可能补全下全家桶).不积跬步无以至千里,万丈高楼咱们先从JS的事件循环机制开始吧,废话

  • 详解JS中你不知道的各种循环测速

    前言 在测试循环速度之前,我们先来创建一个有 100 万数据的数组: const len = 100 * 10000; const arr = []; for (let i = 0; i < len; i++) { arr.push(Math.floor(Math.random() * len)); } 测试环境为: 1.电脑:iMac(10.13.6): 2.处理器:4.2 GHz Intel Core i7: 3.浏览器:Chrome(89.0.4389.82) 1. for 循环 for

  • 详解js中构造流程图的核心技术JsPlumb(2)

    前言:上篇详解js中构造流程图的核心技术JsPlumb介绍了下JsPlumb在浏览器里面画流程图的效果展示,以及简单的JsPlumb代码示例.这篇还是接着来看看各个效果的代码说明. 一.设置连线的样式和颜色效果代码示例 大概的效果如图: 这些效果看着很简单,那么,我们如何用代码去实现它呢.上章我们说过,JsPlumb的连线样式是由点的某些属性决定的,既然如此,我们就通过设置点的样式来动态改变连线的样式即可.来看代码: 首先来看看连线类型的那个select <div id="btn_line

  • 详解vue中v-on事件监听指令的基本用法

    一.本节说明 我们在开发过程中经常需要监听用户的输入,比如:用户的点击事件.拖拽事件.键盘事件等等.这就需要用到我们下面要学习的内容v-on指令. 我们通过一个简单的计数器的例子,来讲解v-on指令的使用. 二. 怎么做 定义数据counter,用于表示计数器数字,初始值设置为0 v-on:click 表示当发生点击事件的时候,触发等号里面的表达式或者函数 表达式counter++和counter--分别实现计数器数值的加1和减1操作 语法糖:我们可以将v-on:click简写为@click 三

  • 详解js中的几种常用设计模式

    工厂模式 function createPerson(name, age){ var o = new Object(); // 创建一个对象 o.name = name; o.age = age; o.sayName = function(){ console.log(this.name) } return o; // 返回这个对象 } var person1 = createPerson('ccc', 18) var person2 = createPerson('www', 18) 工厂函数

  • 详解js中的原型,原型对象,原型链

    理解原型 我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法.看如下例子: function Person(){ } Person.prototype.name = 'ccc' Person.prototype.age = 18 Person.prototype.sayName = function (){ console.log(this.name); } var person1 = ne

  • 详解JS中的reduce fold unfold用法

    fold(reduce) 说说reduce吧, 很喜欢这个函数,节省了不少代码量,而且有一些声明式的雏形了,一些常见的工具函数,flatten,deepCopy,mergeDeep等用reduce实现的很优雅简洁.reduce也称为fold,本质上就是一个折叠数组的过程,把数组中的多个值经过运算变成一个值,每次运算都会有一个函数处理,这个函数就是reduce的核心元素,称之为reducer,reducer函数是个2元函数,返回1个单值,常见的add函数就是reducer const addRed

  • 详解JS中continue关键字和break关键字的区别

    目录 1.框架 2.简单介绍 3.代码演示 4.演示break 1.框架 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <script> </script> </body> </html> 2.简单介绍 1.在javascr

  • 详解JS中异常与错误处理的正确方法

    目录 简介 1 面向错误编程 1.1 墨菲定律 1.2 先判否 2. js 内置的错误处理 2.1 Error 类 2.2 throw 2.3 try catch 2.4 Promise.catch 3. 错误处理只有一次 总结 简介 首先,这篇文章一定会引起争议,因为对于错误处理从来就没有真正的标准答案,每个人都会有自己的主观意见. 我的理解毕竟也是片面,提出的想法主要是基于个人的经验总结,如果有异议,欢迎交流讨论. 为了能够尽量保持客观,我会将处理思想尽量前置,再围绕处理思想展开. 这样大家

随机推荐