• 浏览器和Node 中的Event Loop


    前言

    js与生俱来的就是单线程无阻塞的脚本语言。 作为单线程语言,js代码执行时都只有一个主线程执行任务。
    无阻塞的实现依赖于我们要谈的事件循环。eventloop的规范是真的苦涩难懂,仅仅要理解的话,不推荐去硬啃。

    进程与线程

    一直在说js是单线程语言。那么什么是线程呢,对于大部分前端同学来说,可能并不是那么清晰。推荐阮大佬的这篇文章,形象生动
    首先,计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

    进程

    进程就好比工厂的车间,它代表CPU所能处理的单个任务。
    任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
    即资源分配的最小单位,拥有独立的堆栈空间和数据存储空间

    线程

    线程就好比车间里的工人。车间的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
    即程序执行的最小单位,一个进程可以包括多个线程。

    相对于进程来说,线程不涉及数据空间的操作,所以切换更高效,开销小。

    js单线程的起源

    显然多进程可以并行处理,提升cpu的利用率。
    但是js初期是作为脚本出现的,其要与DOM进行交互,以完成对用户的展示。
    如果多进程,同时操作DOM,那么后果就不可控了。
    例如:对于同一个按钮,不同的进程赋予了不同的颜色,到底该怎么展示。

    作为一个脚本语言,如果使用多线程+锁的话太多复杂了,所以js就是单线程了。

    不过随着js的发展,承载的能力越来越多,局限于单线程使得js的效率等有所限制。
    因此增加了web worker来执行非dom的操作。

    不过该线程非主线程有一些限制、例如不能操作DOM等,也就是为了保证DOM操作的一致性,这里就先不关注了。

    我们主要关注的还是非阻塞的能力基础,即事件循环。

    浏览器中的事件循环

    说道事件循环就要先说事件队列。
    在主线程运行时,会产生堆(heap)和栈(stack)。

    堆中存的是我们声明的object类型的数据,栈中存的是基本数据类型以及函数执行时的运行空间。

    主线程从任务队列中读取事件,这个过程是循环不断的,所以这种运行机制即Event Loop。

    对于同步代码,是直接执行的。 而执行异步方法时同样会加入事件队列中,但是异步事件是有差别的,差别在于执行的优先级不同。

    事件分类

    因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

    • 以下事件属于宏任务:

    setTimeout, setInterval, setImmediate,I/O, UI rendering

    • 以下事件属于微任务

    Promise,Object.observe(已废弃),MutationObserver(html5新特性),process.nextTick

    执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行
    当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

    对于不同类型的任务执行顺序如下:

    1. 同步代码执行
    2. event-loop start
    3. microTasks 队列开始清空(执行)
    4. 检查 Tasks 是否清空,有则跳到 4,无则跳到 6
    5. 从 Tasks 队列抽取一个任务,执行
    6. 检查 microTasks 是否清空,若有则跳到 2,无则跳到 3
    7. 结束 event-loop

    大概流程图如下:

    不如直接看个栗子:

    setTimeout(function () {
        console.log(1);
    });
    
    new Promise(function(resolve,reject){
        console.log(2)
        resolve(3)
    }).then(function(val){
        console.log(val);
    })
    // 2 3 1 
    
    1. 区分事件类型:宏任务setTimeout,微任务.then
    2. 同步代码执行 输出2
    3. 微任务队列清空 输出 3
    4. 宏任务执行 输出 1

    下面来个稍微复杂的:

    setTimeout(()=>{
        console.log('A');
    },0);
    var obj={
        func:function () {
            setTimeout(function () {
                console.log('B')
            },0);
            return new Promise(function (resolve) {
                console.log('C');
                resolve();
            })
        }
    };
    obj.func().then(function () {
        console.log('D')
    });
    console.log('E');
    // c,e,d,b,a
    

    大家可以结合例子自己试下。

    node中的事件循环机制

    在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。
    node中事件循环的实现是依靠的libuv引擎。
    我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,
    而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。
    因此实际上node中的事件循环存在于libuv引擎中。

    而node 事件分为下面几大阶段:

    • timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
    • I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
    • idle, prepare: 仅内部使用。
    • poll: 获取新的I/O事件;node会在适当条件下阻塞在这里,等待新的I/O。
    • check: pool阶段之后,执行setImmediate()设定的回调。
    • close callbacks: 执行比如socket.on('close', ...)的回调

    poll阶段

    值得额外关注的是poll阶段

    该阶段有如下功能:

    1. 执行 timer 阶段到达时间上限的的任务。
    2. 执行 poll 阶段的任务队列。

    如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况

    • 如果 poll 队列不为空的话,会执行 poll 队列直到清空或者系统回调数达到上限
    • 如果 poll 队列为空 ​ 如果设定了 setImmediate 回调,会直接跳到 check 阶段。 如果没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并立即执行。

    process.nextTick()

    nextTick 比较特殊,它有自己的队列,并且,独立于event loop。 它的执行也非常特殊,无论 event loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。

    直接看例子吧:
    process.nextTick

    process.nextTick(function A() {
      console.log(1);
      process.nextTick(function B(){console.log(2);});
    });
    
    setTimeout(function timeout() {
      console.log('TIMEOUT FIRED');
    }, 0)
    // 1
    // 2
    // TIMEOUT FIRED
    

    大概顺序如下:

    1. 因为nextTick的特殊性,当前阶段执行完毕,就执行。所以直接,输出1 2
    2. 执行到timer 输出 TIMEOUT FIRED

    setImmediate

    setImmediate(function A() {
      console.log(1);
      setImmediate(function B(){console.log(2);});
    });
    
    setTimeout(function timeout() {
      console.log('TIMEOUT FIRED');
    }, 0);
    

    这个结果不固定,同一台机器测试结果也有两种:

    // TIMEOUT FIRED =>1 =>2
    或者
    //  1=>TIMEOUT FIRED=>2
    
    1. 事件队列进入timer,性能好的 小于1ms,则不执行回调继续往下。若此时大于1ms, 则输出 TIMEOUT FIRED 就不输出步骤3了。
    2. poll阶段任务为空,存在setImmediate 直接进入setImmediate 输出1
    3. 然后再次到达timer 输出 TIMEOUT FIRED
    4. 再次进入check 阶段 输出 2

    原因在于setTimeout 0 node 中至少为1ms,也就是取决于机器执行至timer时是否到了可执行的时机。

    做个对比就比较清楚了:

    setImmediate(function A() {
      console.log(1);
      setImmediate(function B(){console.log(2);});
    });
    
    setImmediate(function B(){console.log(4);});
    setTimeout(function timeout() {
      console.log('TIMEOUT FIRED');
    }, 20);
    // 1=>2=>TIMEOUT FIRED
    

    此时间隔时间较长,timer阶段最后才会执行,所以会先执行两次check,出处1,2
    下面再看个例子
    poll阶段任务队列

    var fs = require('fs')
    
    fs.readFile('./yarn.lock', () => {
        setImmediate(() => {
            console.log('1')
            setImmediate(() => {
                console.log('2')
            })
        })
        setTimeout(() => {
            console.log('TIMEOUT FIRED')
        }, 0)
        
    })
    // 结果确定:
    // 输出始终为1=>TIMEOUT FIRED=>2
    
    1. 读取文件,回调进入poll阶段
    2. 当前无任务队列,直接check 输出1 将setImmediate2加入事件队列
    3. 接着timer阶段,输出TIMEOUT FIRED
    4. 再次check阶段,输出2

    小结

    浏览器的事件循环
    浏览器比较清晰一些,就是固定的流程,当前宏任务结束,就是执行所有微任务(不一定是全部,可能基于系统能力,会有所剩下),然后再下一个宏任务,微任务这样交替进行。
    node中的事件循环
    主要是把握不同阶段和特殊情况的处理,特别是poll阶段和 process.nextTick任务。

    结束语

    参考文章:

    https://zhuanlan.zhihu.com/p/47152694
    https://html.spec.whatwg.org/multipage/webappapis.html#event-loop
    http://www.ruanyifeng.com/blog/2014/10/event-loop.html
    https://hackernoon.com/understanding-js-the-event-loop-959beae3ac40
    https://juejin.im/post/5bac87b6f265da0a906f78d8
    感谢上述参考文章,关于事件循环这里就总结完毕了,作为自己的一个学习心得。希望能帮助到有需求的同学,一起进步。

  • 相关阅读:
    网络CCNA基础了解
    KVM 安装 VMware 虚拟机
    [转载]JS浏览器兼容性问题
    java中数组是不是对象?
    [转载]request.getServletPath()方法
    weblogic下更改jsp不生效的解决办法
    java之args[0]
    docker小demo
    eclipse优化
    [转载]oracle建表语句大全
  • 原文地址:https://www.cnblogs.com/pqjwyn/p/11238020.html
Copyright © 2020-2023  润新知