一篇文章让你搞清楚JavaScript事件循环

目录
  • 前言
  • 宏任务
  • 微任务
  • 事件循环
  • 宏任务与微任务
  • 总结
  • 参考资料

前言

异步函数也是有执行顺序的。本质上来说,JavaScript是单线程语言,不管是在浏览器中还是nodejs环境下。浏览器在执行js代码和渲染DOM节点都是在同一个线程中,执行js代码就无法渲染DOM,渲染DOM的时候就无法执行js代码。如果按照这种同步方式执行,页面的渲染将会出现白屏甚至是报错,特别是遇到一些耗时比较长的网络请求或者js代码,因此在实际开发中一般是通过异步的方式解决。

什么是异步?js是一步一步执行代码的,遇到alert这种阻塞代码时,js将会停止往下执行直到阻塞代码执行完毕。异步就是将函数放在单独的异步队列中,不会产生阻塞,js可以继续往下执行,等到同步代码执行完毕后再执行异步队列中的函数。因此,js会先执行完同步代码,才会执行异步代码。异步函数之间,虽然都是异步,但是还是有相对的执行顺序。

异步函数的执行主要依靠事件循环来处理,本文重点探讨异步的分类(宏任务、微任务)、事件循环以及异步函数的执行顺序。

宏任务

宏任务,也可简单的说成是任务,在下一轮DOM渲染之后执行。常见的宏任务有:

  • setTimeout:设置一个定时器,该定时器会在设置的延迟时间到期后执行一个函数或者指定的代码块。值得注意的是,setTimeout不一定会在延迟时间到达后就立即执行函数,而是会判断执行队列中是否还有函数没有处理,如果没有了并且栈为空,setTimeout才会在延迟时间到达后执行函数。

    // setTimeout 延迟执行不等于到期时立即执行
    let now = new Date().getSeconds();
    setTimeout(() => {
        console.log('this is setTimeout 0');
    }, 0);
    setTimeout(() => {
        console.log('this is setTimeout 200');
    }, 200);
    while(true) {
        if (new Date().getSeconds() - now >= 2) {
            console.log('break out while loop');
            break;
        }
    }

    运行结果

  • break out while loop
    this is setTimeout 0
    this is setTimeout 200

    先执行同步代码,再执行异步。setTimeout(() => {}, 0)表示0毫秒后立即执行函数,但是当前执行队列中还有未处理完的while循环,因此需要等到while循环执行完毕后,才会根据延迟到期时间执行函数。

  • setInterval:设置定时器,表示在固定的时间间隔内,重复执行某一函数或者特定的代码块。注意使用setInterval有最小延迟时间限制以及确保执行时间要小于间隔时间,如果执行时间无法确定,则应采用递归调用setTimeout的方式代替。
  • 网络请求:只要是指XMLHttpRequest等网络请求

微任务

微任务,在下一轮DOM渲染之前执行,微任务比宏任务更早执。常见的微任务有:

  • promise:表示一个异步操作最终的结果和返回值,可能会失败,也可能成功。异步函数在执行时,什么时候返回结果是不可预料的,Promise把异步操作的返回值和函数关联起来,保证在异步执行结束后会执行对应的函数,并通过函数返回操作值。这种效果就类似于把异步代码“同步执行”。
  • queueMicrotask:将函数添加到微任务队
console.log('start');
// 微任务队列
Promise.resolve().then(() => {
    console.log('promise then');
});
queueMicrotask(() => {
    console.log('queueMicrotask');
});
console.log('end');

运行结果

start
end
promise then
queueMicrotask

事件循环

因为有异步操作的存在,所以出现了事件循环,如果都是同步操作,一行一行执行代码,事件循环也就失去了用武之地。在了解事件循环前,还需要补充js的执行过程:

js在执行代码时,遇到函数就会将其添加到调用栈中,每一帧都会存储当前函数的参数和局部变量,当一个函数执行完毕,则会从调用栈中弹出,直到栈被清空,那么程序也就执行完毕。在执行的过程中,需要的引用数据都是从堆中获取。

在实际开发中,往往是同步代码和异步代码都有。在js执行时,还是从第一行代码开始执行,遇到函数就将其添加到栈中,然后执行同步操作;如果遇到异步函数,则根据其类型,宏任务就添加到宏任务队列,微任务添加到微任务队列。直到同步代码执行完毕,则开始执行异步操作。

异步操作后于同步操作,异步操作内部也是分先后顺序的。总的来说:

  • 微任务先于宏任务执行
  • 微任务与微任务之间根据先后顺序执行,宏任务与宏任务之间根据延迟时间顺序执行
  • 微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
  • 每个任务的执行都是一次出栈操作,直到栈被清空

微任务比宏任务先执行

console.log('start');
// 宏任务队列
setTimeout(() => {
    console.log('setTimeout');
});
// 微任务队列
Promise.resolve().then(() => {
    console.log('promise then');
});
console.log('end');
// 执行结果
start
end
promise then
setTimeout

微任务在下一轮DOM渲染前执行,宏任务在之后执行

let div = document.createElement('div');
div.innerHTML = 'hello world';
document.body.appendChild(div);
let divList = document.getElementByTagName('div');
console.log('同步任务 length ---', list.length);
console.log('start');
setTimeout(() => {
    console.log('setTimeout length ---', list.length);
    alert('宏任务 setTimeout 阻塞'); // 使用alert阻塞js执行
});
Promise.resolve().then(() => {
    console.log('promise then length ---', list.length);
    alert('微任务 promise then 阻塞);
});
console.log('end');

事件循环

event loop会持续监听是否有异步操作,如果有则添加到对应的队列中,等待执行。例如在宏任务中添加微任务,或者在微任务中添加宏任务,当前任务执行完后,可能还会有新的任务添加到事件循环中。

宏任务与微任务

  • 微任务中创建宏任务

        new Promise((resolve) => {
          console.log('promise 1');
          setTimeout(() => {
            console.log('setTimeout 1');
          }, 500);
          resolve();
        }).then(() => {
          console.log('promise then');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 0);
        });
        new Promise((resolve) => {
          console.log('promise 2');
          resolve();
        })

    运行结果

  • promise 1
    promise 2
    promise then
    setTimeout 2
    setTimeout 1

    解析

    js执行代码,遇到两个Promise,则分别添加到微任务队列,同步代码执行完毕。

    在微任务队列中根据先进先出,第一个Promise先执行,遇到setTimeout,则添加到宏任务队列,resolve()返回执行结果并执行then,事件循环将其继续添加到微任务队列;第一个Promise执行完毕,执行第二个Promise。

    继续执行微任务队列,直到清空队列。遇到setTimeout,并将其添加到宏任务队列

    宏任务队列现在有两个任务待执行,由于第二个setTimeout的延迟事件更小,则优先执行第二个;如果相等,则按照顺序执行。

    继续执行宏任务队列,直到清空队列。

  • 宏任务中创建微任务
        setTimeout(() => {
          console.log('setTimeout 1');
          new Promise((resolve) => {
            console.log('promise 1');
            resolve();
          }).then(() => {
            console.log('promise then');
          })
        }, 500);
        setTimeout(() => {
          console.log('setTimeout 2');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          })
        }, 0);

    运行结果

  • setTimeout 2
    promise 2
    setTimeout 1
    promise 1
    promise then

    解析

    js执行代码,遇到两个setTimeout,将其添加到宏任务队列,同步代码执行完毕

    先检查微任务队列中是否有待处理的,刚开始肯定没有,因此直接执行宏任务队列中的任务。第二个为零延迟,需要优先执行。遇到Promise,将其添加到微任务队列。第一个宏任务执行完毕

    在执行第二个宏任务时,微任务队列中已经存在待处理的,因此需要先执行微任务。

    微任务执行完毕,并且延迟时间到期,第一个setTimeout开始执行。遇到Promise,将其添加到微任务队列中

    执行微任务队列中的Promise,执行完毕后遇到then,则将其继续添加到微任务队列

    直到所有微任务执行完毕

  • 宏任务中创建宏任务
        setTimeout(() => {
          console.log('setTimeout 1');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 3');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 4');
          }, 100);
        }, 0);

    运行结果

  • setTimeout 1
    setTimeout 4
    setTimeout 2
    setTimeout 3

    解析

    宏任务中创建宏任务,执行顺序一般来说是按照先后顺序的。对于setTImeout来说,延迟时间相同,则按照先后顺序执行;延迟时间不同,则按照延迟时间的大小先后顺序执行

  • 微任务中创建微任务
        new Promise((resolve) => {
          console.log('promise 1');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          });
          new Promise((resolve) => {
            console.log('promise 3');
            resolve();
          })
          resolve();
        })

    运行结果

  • promise 1
    promise 2
    promise 3

    解析

    微任务中创建微任务,执行顺序一般来说是按照先后顺序执行的。

总结

  • 同步代码直接执行,异步代码添加到宏任务队列或者微任务队列
  • 微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
  • 事件循环持续监听
  • 如果存在异步操作,需要将关联代码放在异步函数中执行;或者将异步函数转为同步操作
  • 如果代码层次比较复杂,同步、异步代码混杂,一定要理清代码的执行顺序。避免因为异步,导致代码出现难以察觉的bug

参考资料

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

(0)

相关推荐

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

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

  • 理解JS事件循环

    伴随着JavaScript这种web浏览器脚本语言的普及,对它的事件驱动交互模型,以及它与Ruby.Python和Java中常见的请求-响应模型的区别有一个基本了解,对您是有益的.在这篇文章中,我将解释一些JavaScript并发模型的核心概念,包括其事件循环和消息队列,希望能够提升你对一种语言的理解,这种语言你可能已经在使用但也许并不完全理解. 这篇文章是写给谁的? 这篇文章是针对在客户端或服务器端使用或计划使用JavaScript的web开发人员的.如果你已经精通事件循环,那么这篇文章的大部

  • 实例分析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的事件循环机制剖析

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

  • 一篇文章让你搞清楚JavaScript事件循环

    目录 前言 宏任务 微任务 事件循环 宏任务与微任务 总结 参考资料 前言 异步函数也是有执行顺序的.本质上来说,JavaScript是单线程语言,不管是在浏览器中还是nodejs环境下.浏览器在执行js代码和渲染DOM节点都是在同一个线程中,执行js代码就无法渲染DOM,渲染DOM的时候就无法执行js代码.如果按照这种同步方式执行,页面的渲染将会出现白屏甚至是报错,特别是遇到一些耗时比较长的网络请求或者js代码,因此在实际开发中一般是通过异步的方式解决. 什么是异步?js是一步一步执行代码的,

  • 一篇文章让你搞懂JavaScript 原型和原型链

    本文由葡萄城技术团队原创并首发 转载请注明出处:葡萄城官网 与多数面向对象的开发语言有所不同,虽然JavaScript没有引入类似类的概念(ES6已经引入了class语法糖),但它仍然能够大量的使用对象,那么如何将所有对象联系起来就成了问题.于是就有了本文中我们要讲到的原型和原型链的概念. 原型和原型链作为深入学习JavaScript最重要的概念之一,如果掌握它了后,弄清楚例如:JavaScript的继承,new关键字的原来.封装及优化等概念将变得不在话下,那么下面我们开始关于原型和原型链的介绍

  • 一篇文章带你搞懂JavaScript的变量与数据类型

    目录 前言: 温馨提示: 变量 1.声明 2.赋值 3.二个语法小细节 变量的命名规范 为什么需要数据类型? 简单数据类型(基本数据类型) 数字型 字符串型 String 什么是数据类型的转换 1.转换为字符串 2.转换为数字型(重点) 转化为布尔型 总结 前言: 我不是搞前端,而是搞后端的.本命编程语言是java.学习js的嘛,因为看到室友能做出动态网页,而我只能做出静态网页,再加上下个学期要学所以提前来学习学习. 温馨提示: java和javsScript没有半毛钱关系,只是javaScri

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定SpringBoot不重启项目实现修改静态资源

    一.通过配置文件控制静态资源的热部署 在配置文件 application.properties 中添加: #表示从这个默认不触发重启的目录中除去static目录 spring.devtools.restart.exclude=classpath:/static/** 或者使用: #表示将static目录加入到修改资源会重启的目录中来 spring.devtools.restart.additional-paths=src/main/resource/static 此时对static 目录下的静态

  • 一篇文章带你搞定 springsecurity基于数据库的认证(springsecurity整合mybatis)

    一.前期配置 1. 加入依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> &

  • 一篇文章带你搞定Ubuntu中打开Pycharm总是卡顿崩溃

    由于 Ubuntu 中的汉字输入实在是太不友好了,所以装了个 搜狗输入法,好不容易把 搜狗输入法装好,本以为可以开开心心的搞代码了,然而... pycharm 一打开,就崩溃,关不掉,进程杀死还是不行,只能关机重启. 本以为 pycharm 出现了问题,又重装了两遍,还是不行. 最终发现竟然是搜狗输入法以及 fcitx 输入法的锅 唉,只能老老实实的把 fctix 和搜狗输入法卸载了: (1)Ubuntu 软件里卸载 fctix,然后将键盘输入法系统改成 IBus (2)卸载搜狗输入法 先查找软

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你搞定Python多进程

    目录 1.Python多进程模块 2.Python多进程实现方法一 3.Python多进程实现方法二 4.Python多线程的通信 5.进程池 1.Python多进程模块 Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Process对象来创建一个进程对象.这个进程对象的方法和线程对象的方法差不多也有start(), run(), join()等方法,其中有一个方法不同Thread线

  • 一篇文章带你搞懂Java线程池实现原理

    目录 1. 为什么要使用线程池 2. 线程池的使用 3. 线程池核心参数 4. 线程池工作原理 5. 线程池源码剖析 5.1 线程池的属性 5.2 线程池状态 5.3 execute源码 5.4 worker源码 5.5 runWorker源码 1. 为什么要使用线程池 使用线程池通常由以下两个原因: 频繁创建销毁线程需要消耗系统资源,使用线程池可以复用线程. 使用线程池可以更容易管理线程,线程池可以动态管理线程个数.具有阻塞队列.定时周期执行任务.环境隔离等. 2. 线程池的使用 /** *

随机推荐