一、前言

JavaScript语言的特点是单线程,单线程只是指主线程,但不管是浏览器执行环境还是node执行环境,除了主线程还有其他的线程,如:网络线程,定时器触发线程,事件触发线程等等,这些线程是如何与主线程协同工作的呢?

二、任务队列

这里不得不提一个任务队列的概念,js代码中所有代码分两种:同步任务、异步任务。

  • 所有同步任务都在主线程上执行,形成一个执行栈;

  • 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件;

  • 一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

  • 主线程不断重复上一步。

三、宏任务和微任务

浏览器和node中宏任务和微任务是不同的,后面详细说明。下面先来了解宏任务和微任务的概念,宏任务和微任务都是任务队列里面的,可以想象成任务队列中其实有两列,宏任务是一列,微任务是一列。

1、宏过任务

首先我们把任务队列里面的任务称为task,浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...),宏任务就是上述的 任务队列里的任务,严格按照时间顺序压栈和执行。如 setTimeOut、setInverter等,下图为浏览器与node中的宏任务。

2、微任务

微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,比如对一系列动作做出反馈,或或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。下图为浏览器与node中的微任务。

四、事件环(Event Loop)

主线程从任务队列中读取事件,这个过程是循环不断的,这个运行机制被称为Event Loop(事件环)

五、浏览器的事件环及对宏任务微任务的执行机制

主线程运行的时候,产生堆和栈,heap就是堆,堆里面是存的是各种对象和函数,stack是栈,var a=1就存储在栈内;dom事件,ajax请求,定时器等异步操作的回调会被放到任务队列callback queue中,这个队列时先进先出的顺序,主线程执行完毕之后会依次执行callback queue中的任务,对应的异步任务就会结束等待状态,进入主线程被执行。

1、浏览器的宏任务和微任务

当stack执行栈空的时候,立即执行microtask checkpoint ,microtask checkpoint 会检查整个微任务队列。所以就会执行微任务队列中所有的任务,才会去执行第一个宏任务,执行完第一个宏任务后,又会去清空微任务队列。

具体支持分类如下: macro-task: setTimeout, setInterval, setImmediate, I/O, UI rendering,mesageChannel micro-task: Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver

我们用下面一段代码来检验一下是否理解浏览器事件环:

setTimeout(function(){console.log('setTimeout1')Promise.resolve().then(()=>{console.log('then1');})
},0)Promise.resolve().then(()=>{console.log('then2');Promise.resolve().then(()=>{console.log('then3');})setTimeout(function(){console.log('setTimeout2')},0)
})
复制代码

执行结果是then2 then3 setTimeout1 then1 setTimeout2

首先代码里面的setTimeout和Promise都是异步任务,js从上到下执行代码,分别将这两个异步任务放到了宏任务队列和微任务队列,执行栈此时为空先清空微任务队列,所以先输出了then2,然后在微任务队列中有添加一个then3的promise任务,在宏任务中添加了一个setTimeout2的定时器任务,所以接着执行下一个微任务,所以输出了then3,开始执行第一个宏任务,输出setTimeout1,并且在微任务队列又添加then1的promise任务,所以转去执行微任务,输出then1,再去执行一个宏任务,就是之前放进去的setTimeout2.

六、node Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

node的代码虽然也是运行在V8引擎上的,但是他还有一个libuv库,专门处理异步i/o操作的,libuv库底层是靠多线程加阻塞I/O模拟实现的异步i/o实现的。 根据上图,Node.js的运行机制如下:

  • 1、我们写的js代码会交给v8引擎进行解析;
  • 2、代码中可能会调用node api,node会交给libuv库处理
  • 3、libuv通过阻塞i/o和多线程实现了异步i/o
  • 4、通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。

Node在进程启动时,便会创建一个类似于while(true)的循环,每执行一次循环体的过程被称为tick,中文翻译应该意为“滴答”,就像时钟一样,每滴答一下,就表示过去了1s。这个tick也有点这个意思,每循环一次,都表示本次tick结束,下次tick开始。每个tick开始之初,都会检查是否有事件需要处理,如果有,就取出事件及关联的callbak函数,如果存在有关联的callback函数,就把事件的结果作为参数调用这个callback函数执行。如果不在有事件处理,就退出进程。

那么在每个tick的过程中,如何判断是否有事件需要处理,先要引入一个概念,叫做“观察者”(watcher)。每一个事件循环都有一个或者多个观察者,判断是否有事件要处理的过程就是向这些观察者询问是否有需要处理的事件

Node的观察者有这样几种:

  • 定时器观察者:setTimeout,setInterval

  • idle观察者:顾名思义,就是早已等在那里的观察者,以后会说到的process.nextTick就属于这类

  • I/O观察者:顾名思义,就是I/O相关观察者,也就是I/O的回调事件,如网络,文件,数据库I/O等

  • check观察者:顾名思义,就是需要检查的观察者,后面会说到的setImmediate就属于这类

事件循环是一个典型的生产者/消费者模型。异步I/O,网络请求,setTimeout等都是典型的事件生产者,源源不断的为Node提供不同类型的事件,这些事件被传到对应的观察者那里,事件循环在每次tick时则从观察者那里取出事件并处理。

我们现在知道,JavaScript的异步I/O调用过程中,回调函数并不由我们开发者调用,事实上,在JavaScript发起调用到内核执行完I/O操作的过程中,存在一种中间产物,它叫做请求对象。这个请求对象会重新封装回调函数及参数,并做一些其他的处理。这个请求对象,会在异步事件完成时被调用,取出回调函数和参数,并传入执行结果进行回调。

组装好请求对象,送入I/O线程池等待执行,实际上只是完成了异步I/O的第一步;第二步则是异步I/O被线程池处理结束后的回调,也就是执行回调。

应该说,事件循环、观察者、请求对象、I/O线程池,这四者共同组成了Node异步I/O模型的基本要素。

不同类型的观察者,处理的优先级不同,idle观察者最先,I/O观察者其次,check观察者最后。

setTimeout()和setInterval()分别用于单次和多次运行任务,其创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick运行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过则形成一个事件,其回调函数马上运行。

1、Node中宏任务和微任务

执行机制:

1、初始化Event loop;

2、执行主代码,遇到异步处理,就分配给对应的队列,直到主代码执行完毕;

3、主代码中遇到所有的微任务,先去执行所有的nextTick(),然后执行其他的微任务,就是nextTick()在微任务里面等级最高;

4、开始Event loop,就是上面的各个观察者按顺序检查;

5、每次执行完毕一个观察者队列,转下一个观察者之前,会清空微任务队列;

6、timer阶段的定时器是不准的,在超过规定时间后,一旦得到执行机会就立即执行。

promise的then是微任务,process.nextTick()也是微任务,执行顺序是nextTick大于then

Promise.resolve().then(()=>{console.log('then');
})
process.nextTick(()=>{console.log('nextTick');
})
复制代码

上面代码先输出nextTick,后输出then 我们可以利用process.nextTick是异步任务,并且执行快的特点实现一些巧妙的解决办法。

class A{constructor(){this.arr=[];process.nextTick(()=>{   console.log(this.arr);})}add(val){this.arr.push(val);}
}
let a=new A();
a.add('123');
a.add('456');
复制代码

假如我们这里没有加process.nextTick的时候,这里打印出来的空数组,因为new实例的时候,就执行了constructor了,但是加了这个process.nextTick后,里面的代码会等同步代码先执行完毕后再执行,这是就已经拿到了数据。打印出['123','456']。

setTimeout(()=>{console.log('timeout1');process.nextTick(()=>{console.log('nextTick');})
},1000)
setTimeout(()=>{console.log('timeout2')
},1000)复制代码

输出:timeout1 timeout2 nextTick 先清空时间队列,去执行下一个队列之前,先去清空微任务队列,也就是idle队列,所以顺序是这样的

setTimeout(()=>{console.log('timeout1');process.nextTick(()=>{console.log('nextTick1');})
},1000)
process.nextTick(()=>{setTimeout(()=>{console.log('timeout2')},1000)console.log('nextTick2');
})
复制代码

上面代码的执行顺序是不固定的,有时候

nextTick2 timeout1 nextTick1 timeout2

nextTick2 timeout1 timeout2 nextTick1

timer阶段的定时器是不准的,他是在超过规定时间后,一旦得到执行机会就立即执行。

上面代码,先走idle队列,先输出nextTick2是固定的,这时候定时器队列中放了两个定时器了。肯定是限制性timeout1,因为他是先放进去的,但是第一个定时器执行完毕后,第二个定时器不一定到结束时间,所以就会去执行idle队列,输出nextTick1,之后再执行timeout2。

第一个定时器是1000毫秒,但是第二个定时器的结束时间可能是1000.8ms,因为process。nextTick也需要执行时间。第一个定时器执行完之后,可能还没到1000.8ms,所以他就去清空了idle任务队列,如果第一个定时器执行完毕后,已经到了1000.8ms,那么肯定先执行第二个定时器。

所以定时器的时间在底层实现的时候是不一样的。

又一个例子

setImmediate(()=>{console.log('setImmediate');
})
setTimeout(()=>{console.log('setTimeout');
},0);  //规范是4ms,这里规定的时间0,在底层实现的时候不是0ms
复制代码

输出:谁都可能先输出

我们知道setImmediate是check检查队列中的,node执行栈执行时间如果是5ms,那么走到时间队列的时候,定时器时间就已经到了,所以先执行setTimeout,再执行setImmediate,但是也有可能node执行栈中代码执行了2ms,没到4ms,就会先走setImmediate,再走时间队列。

let fs=require('fs');
fs.readFile('./1.txt',function(){setImmediate(()=>{console.log('setImmediate');})setTimeout(()=>{console.log('setTimeout');},0);
})
复制代码

文件读取会走poll轮询阶段,得到回调信息后,下一阶段是check阶段,所以setImmediate永远先走。执行结果顺序永远一样

最后一个小测试

let fs=require('fs');
setImmediate(()=>{Promise.resolve().then(()=>{console.log('then1');})
},0)
Promise.resolve().then(()=>{console.log('then2');
})
fs.readFile('./1.txt',function(){process.nextTick(()=>{console.log('nextTick');})setImmediate(()=>{console.log('setImmediate');})
})
复制代码

答案在下面哦~

then2 then1 nextTick setImmediate

第一次肯定是执行微任务输出then2,然后走poll阶段文件读取,文件读取不是立刻执行回调函数的,因为异步任务需要时间等待读取结果,执行栈也不是在等着他执行完毕的,直接执行check阶段,执行setImmediate的回调函数,里面遇到了微任务,现在微任务队列被添加进去一个,在执行fs的回调之前,清空微任务队列,所以输出then1,接着执行fs的回调,添加进去nextTick微任务,check阶段的setImmediate,走完poll阶段,肯定要去清空微任务队列,输出nextTick,再走check阶段,输出setImmediate。

转载于:https://juejin.im/post/5b6aea43f265da0f894b956b

JavaScript运行机制:event-loop相关推荐

  1. 第七期:详解JavaScript运行机制(Event Loop)

    在浏览器中,每个渲染进程都有一个主线程,主线程非常繁忙,既要处理DOM,又要计算样式,还要处理布局,同时还需要处理JavaScript任务以及各种输入事件.此时我们就需要一个系统来统筹调度这么多不同类 ...

  2. 运行指定代码_JavaScript 运行机制(Event Loop)详解

    一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. Java ...

  3. 我看朴灵评注阮一峰的《JavaScript 运行机制详解:再谈Event Loop》

    阮一峰和朴灵对我来说都是大牛,他们俩的书我都买过,阮老师的译作<软件随想录>和朴灵的<深入浅出node.js>.这个事情已经过了4个月了,所以我拿来讲应该也没啥问题. 这件事情 ...

  4. JAVA script 循环 图片_深入分析JavaScript 事件循环(Event Loop)

    事件循环(Event Loop),是每个JS开发者都会接触到的概念,但是刚接触时可能会存在各种疑惑. 众所周知,JS是单线程的,即同一时间只能运行一个任务.一般情况下这不会引发问题,但是如果我们有一个 ...

  5. JavaScript 运行机制详解(理解同步、异步和事件循环)

    1.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. Java ...

  6. javascript运行机制

    阮一峰的网络日志 » 首页 » 档案 搜索 上一篇:乔布斯的管理课 下一篇:编译器的工作过程 分类: JavaScript JavaScript 运行机制详解:再谈Event Loop 作者: 阮一峰 ...

  7. 深入浅出JavaScript运行机制

    一.引子 本文介绍JavaScript运行机制,这一部分比较抽象,我们先从一道面试题入手: console.log(1); setTimeout(function(){ console.log(3); ...

  8. 事件循环机制(event loop)

    目录 前言 一.事件循环概述 二.进程和线程 三.为什么JavaScript是单线程? 四.JavaScript如何解决单线程阻塞问题? 五.JavaScript 运行机制 六.任务队列 task q ...

  9. 一篇文章搞懂JavaScript运行机制

    单线程的JavaScript: 众所周知JavaScript这门语言是单线程,但是为什么要设计成单线程呢?明明多线程更加有效率. 这里我们就要从JavaScript的用途来考虑,JavaScript是 ...

  10. 傻傻分不清的javascript运行机制

    学习到javascript的运行机制时,有几个概念经常出现在各种文章中且容易混淆.Execution Context(执行环境或执行上下文),Context Stack (执行栈),Variable ...

最新文章

  1. IPsec在企业网中的应用
  2. PAT-乙级-1021. 个位数统计 (15)
  3. mysql错误修改数据_使用正则表达式快速修改mysql中错误的varchar类型数据
  4. linux6.5声卡驱动安装,详解CentOS 6.5如何安装Realtek无线网卡驱动
  5. JVM工作原理和特点
  6. 微服务跨数据库联合查询_MySQL数据库联合查询
  7. 小型移动 webApp Demo 知识点整理
  8. JMeter4.0使用笔记 使用Badboy录制脚本,使用代理录制脚本
  9. Spring Boot 执行流程
  10. 64位win10安装不了64位java(点了安装没反应)
  11. snakeyaml生成yaml文件空值显示问题
  12. 深度学习:透过神经网络的内在灵魂与柏拉图的哲学理念
  13. linux内核时区文件编辑器——ZIC时区编辑
  14. 计算机学院运动会通讯稿,大学运动会通讯稿300字汇总七篇
  15. 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java基于java网上心理咨询系统50fxl
  16. 借Kinect 扫描 软件 reconstructMe skanect ksan3d
  17. 打开计算机不显示硬盘一直在加载中,硬盘加载不出来怎么办 下面5个步骤帮你解决...
  18. 【书籍】——机器学习与深度学习
  19. C语言磁盘文件由,C语言对磁盘文件进行快速排序简单实例
  20. java 匿名接口_匿名内部类实现接口

热门文章

  1. android 自定义字体_Android自定义字体教程
  2. mapreduce代码示例_MapReduce算法示例
  3. java初学者书籍_面向初学者的5本最佳Java核心书籍
  4. c语言如何将8个字符串串联_C ++中的字符串串联:串联字符串的4种方法
  5. 编译运行一个java程序_如何从另一个Java程序编译和运行Java程序
  6. adalm pluto_将Apache Pluto与Lucene搜索引擎示例教程集成
  7. Web前端开发如何提高技术水平!
  8. 微服务化浪潮中,网易考拉借浪拉动业务极速增长
  9. Ubuntu普通用户使用串口设备
  10. 最小二乘原理求解线性回归方程