资讯专栏INFORMATION COLUMN

麻烦把JS的事件环给我安排一下!!!

layman / 2624人阅读

摘要:上一轮循环中有少数的会被延迟到这一轮的这一阶段执行。执行,在适当的条件下会阻塞在这个阶段执行设定的回调。

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

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

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

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

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

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

 console.log("1");
 function a(){
    console.log("2");
    function b(){
        console.log("3")
    }
    b()
 }
 a()
 

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

首先我们代码执行的时候会有一个全局上下文,此时代码运行,全局上下文进行执行栈,处在栈底的位置

我们遇到console.log("1"),这个函数在调用的时候进入执行栈,当这句话执行完毕也就是到了下一行的时候我们console这个函数就会出栈,此时栈中仍然只有全局上下文

接着运行代码,这里注意的是我们遇到的函数声明都不会进入执行栈,只有当我们的函数被调用被执行的时候才会进入,这个原理和我们执行栈的名字也就一模一样,接着我们遇到了a();这句代码这个时候我们的a函数就进入了执行栈,然后进入到我们a的函数内部中,此时我们的函数执行栈应该是 全局上下文 —— a

接着我运行console.log("2"),执行栈变成 全局上下文——a——console,接着我们的console运行完毕,我们执行栈恢复成全局上下文 —— a

接着我们遇到了b();那么b进入我们的执行栈,全局上下文——a——b,

接着进入b函数的内部,执行console.log("3")的时候执行栈为全局上下文——a——b——console,执行完毕之后回复成全局上下文——a——b

然后我们的b函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文——a

然后我们的a函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文

然后我们的全局上下文会在我们的浏览器关闭的时候出栈

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

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

执行上下文是单线程

执行上下文是同步执行代码

当有函数被调用的时候,这个函数会进入执行上下文

代码运行会产生一个全局的上下文,只有当浏览器关闭才会出栈

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

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

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

任务队列

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

宏仁务主要是由script(全局任务),setTimeoutsetIntervalsetImmediate ,I/O ,UI rendering

微任务主要是process.nextTick, Promise.then, Object.observer, MutationObserver.

浏览器事件环

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

执行代码,遇到宏仁务放入宏仁务队列,遇到微任务放入微任务队列,执行其他函数的时候放入执行上下文

执行上下文中全部执行完毕后,执行微任务队列

微任务队列执行完毕后,再到宏仁务队列中取出第一项放入执行上下文中执行

接着就不停循环1-3的步骤,这就是浏览器环境中的js事件环

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

思考思考思考思考~~~

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

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

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

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

NodeJs 事件环

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

                   ┌───────────────────────────┐
                ┌─>│           timers          │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                │  │     pending callbacks     │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                │  │       idle, prepare       │
                │  └─────────────┬─────────────┘      ┌───────────────┐
                │  ┌─────────────┴─────────────┐      │   incoming:   │
                │  │           poll            │<─────┤  connections, │
                │  └─────────────┬─────────────┘      │   data, etc.  │
                │  ┌─────────────┴─────────────┐      └───────────────┘
                │  │           check           │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                └──┤      close callbacks      │
                   └───────────────────────────┘

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

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的事件环顺序

同步代码执行,清空微任务队列,执行timer阶段的回调函数(也就是setTimeout,setInterval)

全部执行完毕,清空微任务队列,执行pending callbacks阶段的回调函数

全部执行完毕,清空微任务队列,执行idle, prepare阶段的回调函数

全部执行完毕,清空微任务队列,执行poll阶段的回调函数

全部执行完毕,清空微任务队列,执行check阶段的回调函数(也就是setImmediate)

全部执行完毕,清空微任务队列,执行close callbacks阶段的回调函数

然后循环1-6阶段

那我们来练练手~~~

    // 我们来对着我们的执行阶段看看
    let fs = require("fs");
    // 遇到setTimeout 放入timer回调中
    setTimeout(function(){
        Promise.resolve().then(()=>{
            console.log("then1");
        })
    },0);
    // 放入微任务队列中
    Promise.resolve().then(()=>{
        console.log("then2");
    });
    // i/o操作 放入pending callbacks回调中
    fs.readFile("./text.md",function(){
        // 放入check阶段
        setImmediate(()=>{
            console.log("setImmediate")
        });
        // 放入微任务队列中
        process.nextTick(function(){
            console.log("nextTick")
        })
    });

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96724.html

相关文章

  • js异同步

    摘要:完成请问应该如何安排操作流程上面代码采用个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护串行执行我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。 前言 回调地狱showImg(https://segmentfault.com/img/remote/1460000011554165?w=1000&h=710); js异步 Javascript 语...

    leanxi 评论0 收藏0
  • node核心特性理解

    摘要:概述本文主要介绍了我对的一些核心特性的理解,包括架构特点机制核心模块与简单应用。在此期间,主线程继续执行其他任务。延续了浏览器端单线程,只用一个主线程执行,不断循环遍历事件队列,执行事件。 原文地址在我的博客,转载请注明来源,谢谢! node是在前端领域经常看到的词。node对于前端的重要性已经不言而喻,掌握node也是作为合格的前端工程师一项基本功了。知道node、知道后端的一些东西...

    huangjinnan 评论0 收藏0
  • 一个主板维修大佬转行Java成长经历,心酸......

    摘要:考虑了一段时间之后,终于鼓起勇气找到老板离了职,去了一个北京的某培训机构,进行了个月的加工,每天学习到凌晨点,新鲜出炉,满怀信心的去面试。 12年高中毕业后,因高考失误而停止学业,转战维修行业,在经过3月的培训从小白成长维修大佬,在笔记本维修行业中摸爬滚打了,近3年也算是在行业中小有名气,日子过得十分悠闲,每当修好一片主板那种喜悦无法表达。 showImg(https://segmen...

    tinyq 评论0 收藏0
  • vue -- 非父子组件传值,事件总线(eventbus)使用方式

    摘要:我的个人博客地址资源地址非父子组件传值,事件总线的使用方式我的博客地址如果您对我的博客内容有疑惑或质疑的地方,请在下方评论区留言,或邮件给我,共同学习进步。 欢迎访问我的个人博客:http://www.xiaolongwu.cn 前言 先说一下什么是事件总线,其实就是订阅发布者模式; 比如有一个bus对象,这个对象上有两个方法,一个是on(监听,也就是订阅),一个是emit(触发,也就...

    zone 评论0 收藏0
  • 春招:我居然三天就拿到了offer?

    摘要:算法名称描述优点缺点标记清除算法暂停除了线程以外的所有线程算法分为标记和清除两个阶段首1 回顾我的时间线 在本文的开头,先分享一下自己的春招经历吧: 各位掘友大家好,我是练习时长快一年的Android小蔡鸡,喜欢看源码,逛掘金,写技术文章...... 好了好,不开玩笑了OWO,今年春招投了许多简历的,但是被捞的只有阿里,头条和美团,一路下来挺不容易的. 个人认为在春招中运气>性格>三观>技术...

    stormjun 评论0 收藏0

发表评论

0条评论

layman

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<