前言

  我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制。

正文

  1、JavaScript是单线程的

  JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事。在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。因此JS又是一个非阻塞、异步、并发式的编程语言。

  2、同步和异步

  同步和异步的关系就类似于我们在餐厅排队吃饭的时候,每个人必须挨个的排队来进行买饭这个操作,而在这个过程中十分无聊,这时候我们可以边排着队边玩下手机,不需多久就排到了我们买饭。这个排队过程就是JS中的一个同步操作,玩手机就像一个异步操作。同步和异步的差别就在于排队买饭和玩手机这两个任务的执行顺序的不同。

  同步: 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是处于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

  异步: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
  上面的代码会打印  1 》 5 》 2 》 3 》4,为什么会产生这样的结果,我们来看下事件循环。

  3、事件循环

  事件循环过程可以简单描述为:

  a、函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs ,接着执行同步任务,直到 Stack 为空;

  b、在此期间 WebAPIs 完成这个事件,把回调函数放入 CallbackQueue (任务队列)中等待;

  c、当执行栈为空时,Event Loop 把 Callback Queue中的一个任务放入Stack中,回到第1步。

  事件循环(Event Loop) 是让 JavaScript 做到既是单线程,又绝对不会阻塞的核心机制,也是 JavaScript 并发模型(Concurrency Model)的基础,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。在执行和协调各种任务时,Event Loop 会维护自己的事件队列。

  事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务,一个任务开始后直至结束,不会被其他任务中断。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。

  任务队列:在JavaScript中,异步任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务:

  宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。

  而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。

  当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。每次宏任务执行完毕,都会去判断微任务队列是否产生新任务,若存在就优先执行微任务,否则按序执行宏任务。

  事件循环通常至少需要两个任务队列:宏任务队列和微任务队列。两种队列在同一时刻都只执行一个任务。

console.log("script start");

setTimeout(function () {
console.log("setTimeout");
}, 0); Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
}); console.log("script end");

  按照上面的内容,分析执行步骤:

  1、宏任务:执行整体代码(相当于<script>中的代码):

    输出: script start

    遇到 setTimeout,加入宏任务队列,当前宏任务队列(setTimeout)

    遇到 promise,加入微任务,当前微任务队列(promise1)

    输出:script end

  2、微任务:执行微任务队列(promise1)

    输出:promise1,then 之后产生一个微任务,加入微任务队列,当前微任务队列(promise2)

    执行 then,输出promise2

    执行渲染操作,更新界面。

    宏任务:执行 setTimeout

    输出:setTimeout

  注意:new Promise(..)中的代码,也是同步代码,会立即执行。只有then之后的代码,才是异步执行的代码,是一个微任务。

console.log("script start");

setTimeout(function () {
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>]

  宏任务:

    输出: script start

    遇到 timeout1,加入宏任务

    遇到 Promise,输出promise1,直接 resolve,将 then 加入微任务,遇到 timeout2,加入宏任务。

    输出script end

  宏任务第一个执行结束

  当前任务队列:微任务[then1],宏任务[timeou1, timeout2]

  微任务:

    执行 then1,输出then1

  微任务队列清空

  当前任务队列:微任务[],宏任务[timeou1, timeout2]

  宏任务:

    输出timeout1

    输出timeout2

  当前任务队列:微任务[],宏任务[timeou2]

  微任务:

    为空跳过

  当前任务队列:微任务[],宏任务[timeou2]

  宏任务:

    输出timeout2

  注意:async 和 await 其实就是 Generator 和 Promise 的语法糖。async 函数和普通 函数没有什么不同,他只是表示这个函数里有异步操作的方法,并返回一个 Promise 对象

async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
// Promise 写法
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => console.log("async1 end"));
}

  下面例子:

async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
setTimeout(() => {
console.log("timeout");
}, 0);
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");

  步骤解析:

  当前任务队列:宏任务:[<script>],微任务: []

  宏任务:

    输出:async1 start

    遇到 async2,输出:async2,并将 then(async1 end)加入微任务

    遇到 setTimeout,加入宏任务。

    遇到 Promise,输出:promise1,直接 resolve,将 then(promise2)加入微任务

    输出:script end

  当前任务队列:微任务[promise2, async1 end],宏任务[timeout]

  微任务:

    输出:promise2

    promise2 出队

    输出:async1 end

    async1 end 出队

    微任务队列清空

  当前任务队列:微任务[],宏任务[timeout]

  宏任务:

    输出:timeout

    timeout 出队,宏任务清空

写在最后

  以上就是本文的全部内容,希望给读者带来些许的帮助和进步,方便的话点个关注,小白的成长之路会持续更新一些工作中常见的问题和技术点。

js--事件循环机制的更多相关文章

  1. js事件循环机制辨析

     对于新接触js语言的人来说,最令人困惑的大概就是事件循环机制了.最开始这也困惑了我好久,花了我几个月时间通过书本,打代码,查阅资料不停地渐进地理解他.接下来我想要和大家分享一下,虽然可能有些许错误的 ...

  2. JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同

    一.JS单线程.异步.同步概念 从上一篇说明vue nextTick的文章中,多次出现“事件循环”这个名词,简单说明了事件循环的步骤,以便理解nextTick的运行时机,这篇文章将更为详细的分析下事件 ...

  3. js 事件循环机制 EventLoop

    js 的非阻塞I/O  就是由事件循环机制实现的 众所周知  js是单线程的 也就是上一个任务完成后才能开始新的任务 那js碰到ajxa和定时器.promise这些异步任务怎么办那?这时候就出现了事件 ...

  4. Node.js 事件循环机制

    Node.js 采用事件驱动和异步 I/O 的方式,实现了一个单线程.高并发的 JavaScript 运行时环境,而单线程就意味着同一时间只能做一件事,那么 Node.js 如何通过单线程来实现高并发 ...

  5. js事件循环机制 (Event Loop)

    一.JavaScript是单线程单并发语言 什么是单线程 主程序只有一个线程,即同一时间片断内其只能执行单个任务. 为什么选择单线程? JavaScript的主要用途是与用户互动,以及操作DOM.这决 ...

  6. js事件循环机制(Event Loop)

    javascript从诞生之日起就是一门  单线程的  非阻塞的  脚本语言,单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务,非阻塞靠的就是 event lo ...

  7. js事件循环机制

    本文参考链接:https://www.jianshu.com/p/cf47bc0bf2ab 一.先搞懂两个东西:堆和栈 栈由操作系统自动分配释放,用于存放函数的参数值.局部变量等一些基本的数据类型,其 ...

  8. JS:事件循环机制、调用栈以及任务队列

    点击查看原文 写在前面 js里的事件循环机制十分有趣.从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的. 在之前,我只是简单地认为由于函数执行很快,setTimeout执行 ...

  9. js的事件循环机制:同步与异步任务(setTimeout,setInterval)宏任务,微任务(Promise,process.nextTick)

    javascript是单线程,一切javascript版的"多线程"都是用单线程模拟出来的,通过事件循环(event loop)实现的异步. javascript事件循环 事件循环 ...

  10. JS JavaScript事件循环机制

    区分进程和线程 进程是cpu资源分配的最小单位(系统会给它分配内存) 不同的进程之间是可以同学的,如管道.FIFO(命名管道).消息队列 一个进程里有单个或多个线程 浏览器是多进程的,因为系统给它的进 ...

随机推荐

  1. windev中自定义选定列的使用和注意事项

    windev系统默认的多选,需要使用Ctrl+或者Shift+来点选,使用并不方便,所以我们一般在首列增加checkbox列,并在行头增加一个checkbox控制,作为全选使用.使用这个方法时,有几个 ...

  2. C++ 派生类函数重载与虚函数继承详解

    目录 一.作用域与名字查找 1.作用域的嵌套 2.在编译时进行名字查找 3.名字冲突与继承 4.通过作用域运算符来使用隐藏的成员 二.同名函数隐藏与虚函数覆盖 1.几种必须区分的情况 2.一个更复杂的 ...

  3. 【C# 】继承

    背景..什么是继承? 「继承」是对象导向编程的其中一个基本属性. 它可让您定义子类,重复使用(继承).扩充或修改父类别行为. 其成员可供继承的类别称为基底类别. 继承基底类别成员的类别则称为「衍生类别 ...

  4. java变量的初始化之后的默认值

    对于类的成员变量 不管程序有没有显示的初始化,Java  虚拟机都会先自动给它初始化为默认值. 1.整数类型(byte.short.int.long)的基本类型变量的默认值为0. 2.单精度浮点型(f ...

  5. 如何给远程桌面发送“Ctrl+Alt+Delete”命令

    转至:https://www.yunweiku.com/forum.php?mod=viewthread&tid=925 当我们远程桌面需要是用ctrl+alt+delete组合键时无法使用, ...

  6. 用WebDev.WebServer40.EXE调试VS代码

    1.找到WebDev.WebServer40的录井,一般位于C:/Program Files (x86)/Common Files/Microsoft Shared/DevServer/10.0/We ...

  7. ibv_get_device_list()函数

    struct ibv_device** ibv_get_device_list(int *num_devices); 描述 函数用来返回一个当前可用的RDMA设备数组. 注意 数组以NULL结尾: R ...

  8. LeetCode-129-求根节点到叶节点数字之和

    求根节点到叶节点数字之和 题目描述:给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字. 每条从根节点到叶节点的路径都代表一个数字: 例如,从根节点到叶节点的路径 1 ...

  9. 矩池云上编译安装dlib库

    方法一(简单) 矩池云上的k80因为内存问题,请用其他版本的GPU去进行编译,保存环境后再在k80上用. 准备工作 下载dlib的源文件 进入python的官网,点击PyPi选项,搜索dilb,再点击 ...

  10. svelte组件:Svelte3自定义Navbar+Tabbr组件|svelte自定义插件

    基于Svelte3自定义组件Navbar+Tabbar沉浸式导航条|底部凸起菜单栏 Svelte 一种全新的构建用户界面的框架.当下热门的 Vue 和 React 在浏览器中需要做大量的工作,而 Sv ...