上次大家跟我吃饱喝足又撸了一遍PromiseA+,想必大家肯定满脑子想的都是西瓜可乐......

什么西瓜可乐!明明是Promise!

呃,清醒一下,今天大家搬个小板凳,听我说说JS中比较有意思的事件环,在了解事件环之前呢,我们先来了解几个基本概念。

栈(Stack)

栈是一种遵循后进先出(LIFO)的数据集合,新添加或待删除的元素都保存在栈的末尾,称作栈顶,另一端称作栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底

感觉说起来并不是很好理解,我们举个例子,比如有一个乒乓球盒,我们不停的向球盒中放进乒乓球,那么最先放进去的乒乓球一定是在最下面,最后放进去的一定是在最上面,那么如果我们想要把这些球取出来是不是就必须依次从上到下才能拿出来,这个模型就是后进先出,就是我们后进入球盒的球反而最先出来。

栈的概念其实在我们js中十分的重要,大家都知道我们js是一个单线程语言,那么他单线程在哪里呢,就在他的主工作线程,也就是我们常说的执行上下文,这个执行上下文就是栈空间,我们来看一段代码:


  1. console.log('1');
  2. function a(){
  3. console.log('2');
  4. function b(){
  5. console.log('3')
  6. }
  7. b()
  8. }
  9. a()

我们知道函数执行的时候会将这个函数放入到我们的执行上下文中,当函数执行完毕之后会弹出执行栈,那么根据这个原理我们就能知道这段代码的运行过程是

  1. 首先我们代码执行的时候会有一个全局上下文,此时代码运行,全局上下文进行执行栈,处在栈底的位置
  2. 我们遇到console.log('1'),这个函数在调用的时候进入执行栈,当这句话执行完毕也就是到了下一行的时候我们console这个函数就会出栈,此时栈中仍然只有全局上下文
  3. 接着运行代码,这里注意的是我们遇到的函数声明都不会进入执行栈,只有当我们的函数被调用被执行的时候才会进入,这个原理和我们执行栈的名字也就一模一样,接着我们遇到了a();这句代码这个时候我们的a函数就进入了执行栈,然后进入到我们a的函数内部中,此时我们的函数执行栈应该是 全局上下文 —— a
  4. 接着我运行console.log('2'),执行栈变成 全局上下文——a——console,接着我们的console运行完毕,我们执行栈恢复成全局上下文 —— a
  5. 接着我们遇到了b();那么b进入我们的执行栈,全局上下文——a——b,
  6. 接着进入b函数的内部,执行console.log('3')的时候执行栈为全局上下文——a——b——console,执行完毕之后回复成全局上下文——a——b
  7. 然后我们的b函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文——a
  8. 然后我们的a函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文
  9. 然后我们的全局上下文会在我们的浏览器关闭的时候出栈

我们的执行上下文的执行过程就是这样,是不是清楚了很多~

通过上面的执行上下文我们可以发现几个特点:

  • 执行上下文是单线程
  • 执行上下文是同步执行代码
  • 当有函数被调用的时候,这个函数会进入执行上下文
  • 代码运行会产生一个全局的上下文,只有当浏览器关闭才会出栈

队列(Queue)

队列是一种遵循先进先出(FIFO)的数据集合,新的条目会被加到队列的末尾,旧的条目会从队列的头部被移出。

这里我们可以看到队列和栈不同的地方是栈是后进先出类似于乒乓球盒,而队列是先进先出,也就是说最先进入的会最先出去。
同样我们举个例子,队列就好比是我们排队过安检,最先来到的人排在队伍的首位,后来的人接着排在队伍的后面,然后安检员会从队伍的首端进行安检,检完一个人就放行一个人,是不是这样的一个队伍就是先进先出的一个过程。

队列这里我们就要提到两个概念,宏任务(macro task),微任务(micro task)。

任务队列

Js的事件执行分为宏仁务和微任务

  • 宏仁务主要是由script(全局任务),setTimeoutsetIntervalsetImmediate ,I/O ,UI rendering
  • 微任务主要是process.nextTick, Promise.then, Object.observer, MutationObserver.

浏览器事件环

js执行代码的过程中如果遇到了上述的任务代码之后,会先把这些代码的回调放入对应的任务队列中去,然后继续执行主线程的代码知道执行上下文中的函数全部执行完毕了之后,会先去微任务队列中执行相关的任务,微任务队列清空之后,在从宏仁务队列中拿出任务放到执行上下文中,然后继续循环。

  1. 执行代码,遇到宏仁务放入宏仁务队列,遇到微任务放入微任务队列,执行其他函数的时候放入执行上下文
  2. 执行上下文中全部执行完毕后,执行微任务队列
  3. 微任务队列执行完毕后,再到宏仁务队列中取出第一项放入执行上下文中执行
  4. 接着就不停循环1-3的步骤,这就是浏览器环境中的js事件环

  1. //学了上面的事件环 我们来看一道面试题
  2. setTimeout(function () {
  3. console.log(1);
  4. }, 0);
  5. Promise.resolve(function () {
  6. console.log(2);
  7. })
  8. new Promise(function (resolve) {
  9. console.log(3);
  10. });
  11. console.log(4);
  12. //上述代码的输出结果是什么???

<p style='text-align:center;color:#d14;font-size:13px;'>思考思考思考思考~~~</p>

正确答案是3 4 1,是不是和你想的一样?我们来看一下代码的运行流程


  1. // 遇到setTimeout 将setTimeout回调放入宏仁务队列中
  2. setTimeout(function () {
  3. console.log(1);
  4. }, 0);
  5. // 遇到了promise,但是并没有then方法回调 所以这句代码会在执行过程中进入我们当前的执行上下文 紧接着就出栈了
  6. Promise.resolve(function () {
  7. console.log(2);
  8. })
  9. // 遇到了一个 new Promise,不知道大家还记不记得我们上一篇文章中讲到Promise有一个原则就是在初始化Promise的时候Promise内部的构造器函数会立即执行 因此 在这里会立即输出一个3,所以这个3是第一个输入的
  10. new Promise(function (resolve) {
  11. console.log(3);
  12. });
  13. // 然后输入第二个输出4 当代码执行完毕后回去微任务队列查找有没有任务,发现微任务队列是空的,那么就去宏仁务队列中查找,发现有一个我们刚刚放进去的setTimeout回调函数,那么就取出这个任务进行执行,所以紧接着输出1
  14. console.log(4);

看到上述的讲解,大家是不是都明白了,是不是直呼简单~

那我们接下来来看看node环境中的事件执行环

NodeJs 事件环

浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv标准,因此呢在事件的执行中就会有一定的差异,大家都知道nodejs其实是js的一种runtime,也就是运行环境,那么在这种环境中nodejs的api大部分都是通过回调函数,事件发布订阅的方式来执行的,那么在这样的环境中我们代码的执行顺序究竟是怎么样的呢,也就是我们不同的回调函数究竟是怎么分类的然后是按照什么顺序执行的,其实就是由我们的libuv所决定的。


  1. ┌───────────────────────────┐
  2. ┌─&gt;│ timers
  3. └─────────────┬─────────────┘
  4. ┌─────────────┴─────────────┐
  5. pending callbacks
  6. └─────────────┬─────────────┘
  7. ┌─────────────┴─────────────┐
  8. idle, prepare
  9. └─────────────┬─────────────┘ ┌───────────────┐
  10. ┌─────────────┴─────────────┐ incoming:
  11. poll │&lt;─────┤ connections,
  12. └─────────────┬─────────────┘ data, etc.
  13. ┌─────────────┴─────────────┐ └───────────────┘
  14. check
  15. └─────────────┬─────────────┘
  16. ┌─────────────┴─────────────┐
  17. └──┤ close callbacks
  18. └───────────────────────────┘

我们先来看下这六个任务是用来干什么的

  • timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
  • pending callbacks: 上一轮循环中有少数的 I/O callback会被延迟到这一轮的这一阶段执行。
  • idle, prepare: 仅内部使用。
  • poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

我们再来看网上找到的一张nodejs执行图,我们能看到图中有六个步骤 ,当代码执行中如果我们遇到了这六个步骤中的回调函数,就放入对应的队列中,然后当我们同步人物执行完毕的时候就会切换到下一个阶段,也就是timer阶段,然后timer阶段执行过程中会把这个阶段的所有回调函数全部执行了然后再进入下一个阶段,需要注意的是我们在每次阶段发生切换的时候都会先执行一次微任务队列中的所有任务,然后再进入到下一个任务阶段中去,所以我们就能总结出nodejs的事件环顺序

  1. 同步代码执行,清空微任务队列,执行timer阶段的回调函数(也就是setTimeout,setInterval)
  2. 全部执行完毕,清空微任务队列,执行pending callbacks阶段的回调函数
  3. 全部执行完毕,清空微任务队列,执行idle, prepare阶段的回调函数
  4. 全部执行完毕,清空微任务队列,执行poll阶段的回调函数
  5. 全部执行完毕,清空微任务队列,执行check阶段的回调函数(也就是setImmediate)
  6. 全部执行完毕,清空微任务队列,执行close callbacks阶段的回调函数
  7. 然后循环1-6阶段

那我们来练练手~~~


  1. // 我们来对着我们的执行阶段看看
  2. let fs = require('fs');
  3. // 遇到setTimeout 放入timer回调中
  4. setTimeout(function(){
  5. Promise.resolve().then(()=&gt;{
  6. console.log('then1');
  7. })
  8. },0);
  9. // 放入微任务队列中
  10. Promise.resolve().then(()=&gt;{
  11. console.log('then2');
  12. });
  13. // i/o操作 放入pending callbacks回调中
  14. fs.readFile('./text.md',function(){
  15. // 放入check阶段
  16. setImmediate(()=&gt;{
  17. console.log('setImmediate')
  18. });
  19. // 放入微任务队列中
  20. process.nextTick(function(){
  21. console.log('nextTick')
  22. })
  23. });

首先同步代码执行完毕,我们先清空微任务,此时输出then2,然后切换到timer阶段,执行timer回调,输出then1,然后执行i/o操作回调,然后清空微任务队列,输出nextTick,接着进入check阶段,清空check阶段回调输出setImmediate

所有的规则看着都云里雾里,但是呢只要我们总结出来了规律,理解了他们的运行机制那么我们就掌握了这些规则,好咯,今天又学了这么多,不说了不说了,赶紧滚去写业务代码了.............

来源:https://segmentfault.com/a/1190000015932832

麻烦把JS的事件环给我安排一下的更多相关文章

  1. js键盘事件全面控制详解

      js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件 ...

  2. js键盘事件全面控制

    js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件类型 ...

  3. js键盘事件全面控制详解【转】

    js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件类型 ...

  4. 我已经迷失在事件环(event-loop)中了【Nodejs篇】

    我第一次看到他事件环(event-loop)的时候,我是一脸懵,这是什么鬼,是什么循环吗,为什么event还要loop,不是都是一次性的吗? 浏览器中和nodejs环境中的事件环是有一些区别的,这里我 ...

  5. js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获

    js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获 一.总结 一句话总结:他们是描述事件触发时序问题的术语.事件捕获指的是从document到触发事件的那个节点,即自上而下的去触发事件.相反的,事件 ...

  6. javascript事件环微任务和宏任务队列原理

    哈喽!大家好!我是木瓜太香,我又来嘞,今天来说说前端面试中经常别问到的 JS 事件环问题. JS 事件环 JS 程序的运行是离不开事件环机制的,这个机制保证在发生某些事情的时候我们有机会执行一个我们事 ...

  7. 探讨Js的事件的冒泡阶段

    近来看到了一个新的知识点叫Js的事件冒泡,因此决定自己来研究一番. 大家应该都知道,Js中的事件处理分为三个阶段,1:事件的捕获阶段,2:处于目标阶段,3:事件的冒泡阶段.那么什么是事件的捕获和冒泡呢 ...

  8. 浅谈js的事件冒泡机制

    很多人都听说过,js的事件冒泡机制,其实,这个说法还是比较生动形象的,就是一个水泡在水底下,冒泡到水面的过程. 那js的事件冒泡机制呢,就是一个DOM树,一级一级向上冒的过程,最终是到document ...

  9. Js 冒泡事件阻止

    Js 冒泡事件阻止   1. 事件目标 现在,事件处理程序中的变量event保存着事件对象.而event.target属性保存着发生事件的目标元素.这个属性是DOM API中规定的,但是没有被所有浏览 ...

随机推荐

  1. vue:使用不同参数跳转同一组件,实现动态加载图片和数据,以及利用localStorage和vuex持久化数据

    需求:通过不同的参数复用同一组件,实现动态加载数据和图片,同时,在页面刷新时,图片依旧可以加载成功. 过程出现的bug和问题: 1.使用params传参后,再次刷新页面,参数丢失导致数据无法再次加载 ...

  2. 如何将Map键值的下划线转为驼峰

    本文不再更新,可能存在内容过时的情况,实时更新请移步我的新博客:如何将Map键值的下划线转为驼峰: 例,将HashMap实例extMap键值下划线转为驼峰: 代码: HashMap<String ...

  3. Ionic JPush极光推送二

    1.看图解决问题   2.解决出现统计代码提示问题 修改这个java 文件 导入命名空间 import cn.jpush.android.api.JPushInterface; 添加方法 @Overr ...

  4. C++继承相关知识点总结

    1:派生类继承基类的成员并且可以定义自己的附加成员.每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员. 每个派生类对象都有基类部分,包括基类的private成员.类可以访问共基类的publ ...

  5. 稀疏表示step by step(转)

    原文地址:稀疏表示step by step(转)作者:野火春风 稀疏表示step by step(1)     声明:本人属于绝对的新手,刚刚接触“稀疏表示”这个领域.之所以写下以下的若干个连载,是鼓 ...

  6. 树hash

    判断树的同构,采用树hash的方式. 树hash定义在有根树上.判断无根树同构的时候,可以比较重心为根的hash值或者比较每个点为根的hash值. h[x]表示x为根的子树的hash,g[x]表示x为 ...

  7. Ionic Cordova 环境配置window

    1.安装java jdk http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2.安 ...

  8. java 判断数据类型和方法

    java 判断数据类型和方法 .我从SOLR查询中获取一个数据一,已知数据类型,是string或者int 或者其他 .我有一个方法(set方法),只有一个参数,但是我不知道参数的数据类型,可能是str ...

  9. mybatis深入理解(五)-----MyBatis的一级缓存实现详解 及使用注意事项

    0.写在前面 MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上.MyBatis提供了一级缓存.二级缓存 这两个缓存机制,能够很好地处理和维护缓存,以提高 ...

  10. Java过滤器—Filter用法简介

    一.什么是Filter? Filter译为过滤器. 由于年,Sun公司在Servlet2.3规范中添加了Filter功能,并在Servlet2.4中对Filter进行了细节上的补充. 二.运行原理: ...