一文搞懂Node.js以及浏览器中的事件循环机制

Posted 前端下午茶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞懂Node.js以及浏览器中的事件循环机制相关的知识,希望对你有一定的参考价值。

本文主要梳理node.js,浏览器相关及Event Loop事件循环等,会持续补充更新哦!首先我们要记住JS是一个单线程的语言。

JS同步异步

  • 同步阻塞
  • **异步非阻塞:**在涉及需要等待的操作,我们选择让程序继续运行,在等待时间结束的时候,通知一下我们的程序内容执行完毕,你可以操作这些资源了,这段等待时间并不影响你程序的继续执行,只是在未来的某个时间段(不确定),有一个操作一定会执行。

JS的异步方案演进史

Raw Callback Style -> Promise Callback Style -> Generator Callback Style -> Async/Await Callback
任务队列:先进先出

JS Engine 和 JS Runtime

  • **Engine(执行引擎):**如V8 Engine,V8 实现并提供了 ECMAScript 标准中的所有数据类型、操作符、对象和方法(注意并没有 DOM)。 Event Loop 是属于 JavaScript Runtime 的,是由宿主环境提供的(比如浏览器,node)
  • **Runtime(执行环境):**Chrome 提供了 window、DOM,而 Node.js 则是 require、process 等等。

JS执行机制

事件循环(Event Loop)是js实现异步的一种方法,也是js的执行机制。

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

怎么知道主线程执行栈为空

js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

执行规则

  • 首先在执行栈(call stack)中的内容执行完毕清空后,会在事件队列(Event queue)检查一下哪些是宏任务哪些是微任务,然后执行所有的微任务,然后执行一个宏任务,之后再次执行所有的微任务。也就是说在主线程(main thread)任务执行完毕后会把任务队列中的微任务全部执行,然后再执行一个宏任务,这个宏任务执行完再次检查队列内部的微任务,有就全部执行没有就再执行一个宏任务。
  • JS是单线程但是浏览器是多线程。你的异步任务是浏览器开启对应的线程来执行的,最后放入JS引擎中进行执行。
  • 所以在执行定时器、事件、ajax这些异步事件的时候是另外三个线程在执行代码,并不是JS引擎在做事情,在这些线程达到某一特定事件把任务放入JS引擎的线程中,同时GUI线程(渲染界面html的线程)与JS线程是互斥的,在JS引擎执行时GUI线程会被冻结、挂起。

浏览器主线程常驻线程

  • GUI 渲染线程

    • 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
    • 页面重绘和回流
    • 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
  • JS 引擎线程

    • 负责 JS 脚本代码的执行
    • 负责执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
    • 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
  • 事件触发线程

    • 负责将准备好的事件交给 JS 引擎线程执行
    • 多个事件加入任务队列的时候需要排队等待(JS 的单线程)
  • 定时器触发线程

    • 负责执行异步的定时器类的事件,如 setTimeout、setInterval
    • 定时器到时间之后把注册的回调加到任务队列的队尾
  • HTTP 请求线程

    • 负责执行异步请求
    • 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行

宏任务和微任务

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval,I/O、UI Rendering等
  • micro-task(微任务):Promise.then catch finally(注意不是说 Promise,new promise直接执行),process.nextTick,MutationObserver

setTimeout(fn,0)

指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。(关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。)

setInterval(fn,0)

会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

requestAnimationFrame

**请求动画帧,是一个宏任务,**html5 提供的一个专门用于请求动画的API,相比起setTimeout由系统决定回调函数的执行时机。60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿。

Promise与process.nextTick(callback)

process.nextTick(callback):类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

执行和运行的区别

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

一些特殊的点

  • async 隐式返回 Promise 作为结果,执行完 await 之后直接跳出 async 函数,让出执行的所有权,当前任务的其他代码执行完之后再次获得执行权进行执行
  • 立即 resolve 的 Promise 对象,是在本轮"事件循环"的结束时执行,而不是在下一轮"事件循环"的开始时
  • 在一轮event loop中多次修改同一dom,只有最后一次会进行绘制。
  • 渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
  • 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。

Node下的 Event Loop

基于libuv实现,而libuv是 Node 的新跨平台抽象层,libuv使用异步IO事件驱动的编程方式,核心是提供i/o的事件循环和异步回调。libuv的API包含有时间,非阻塞的网络,异步文件操作,子进程等等。

六个阶段

  • timers:执行setTimeout() 和 setInterval()中到期的callback。
  • pending callback: 上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行
  • idle, prepare:仅内部使用
  • poll: 最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate的callback
  • close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])、http.server.on('close, fn)

Node与浏览器的 Event Loop 差异

  • 浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
  • 而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

node.js 是⼀个 JS 的服务端运⾏环境,简单的来说,是在 JS 语⾔规范的基础上,封装了⼀些服务端的运⾏时 对象,让我们能够简单实现⾮常多的业务功能。> 基于 JS 语法增加与操作系统之间的交互。

Node的简单介绍

底层依赖

node.js 的主要依赖⼦模块有以下内容:

  • V8 引擎:主要是 JS 语法的解析,有了它才能识别JS语法
  • libuv: c 语⾔实现的⼀个⾼性能异步⾮阻塞 IO 库,⽤来实现 node.js 的事件循环
  • http-parser/llhttp: 底层处理 http 请求,处理报⽂,解析请求包等内容
  • openssl: 处理加密算法,各种框架运⽤⼴泛
  • zlib: 处理压缩等内容

常见内置模块

  • fs: ⽂件系统,能够读取写⼊当前安装系统环境中硬盘的数据
  • path: 路径系统,能够处理路径之间的问题
  • crypto: 加密相关模块,能够以标准的加密⽅式对我们的内容进⾏加解密
  • dns: 处理 dns 相关内容,例如我们可以设置 dns 服务器等等
  • http: 设置⼀个 http 服务器,发送 http 请求,监听响应等等
  • readline: 读取 stdin 的⼀⾏内容,可以读取、增加、删除我们命令⾏中的内容
  • os: 操作系统层⾯的⼀些 api,例如告诉你当前系统类型及⼀些参数
  • vm: ⼀个专⻔处理沙箱的虚拟机模块,底层主要来调⽤ v8 相关 api 进⾏代码解析。

Buffer 缓冲

Buffer 类,用来创建一个专门存放二进制数据的缓存区。

  • Buffer 是 UInt8Array
  • 是数组,且每个item的有效范围是 0~255(无符号8位)
  • 详细api可查 [nodejs.cn/api/buffer.…][nodejs.cn_api_buffer.]
    Buffer.from([1, 1, 1, 1]); //Buffer.from() 接口创建Buffer对象(传入的array的元素只能是数字,不然就会自动被0覆盖)
    Buffer.from([257, 257.5, -255, '1']); //都是1
    Buffer.from('abcd'); //utf8编码转换 <Buffer 61 62 63 64>
    const bf = Buffer.alloc(256); //创建一个长度为 256、且用 0 填充的 Buffer
    const len = buf.write("www.baidu.com"); //写入,返回实际写入的大小。如果 buffer 空间不足, 则只会写入部分字符串。
    const str1 = buf.toString('utf8') //// 使用 'utf8' 编码, 并输出: www.baidu.com
    const str2 = buf.toString('utf8',0,9) //// 使用 'utf8' 编码, 并输出: www.baidu

EventEmitter 事件

events 模块只提供了一个对象:events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。

  • **on方法,**注册事件回调
  • **emit方法,**手动触发事件
  • 详细api可查** **[nodejs.cn/api/events.…][nodejs.cn_api_events.]
    const EventEmitter = require('events');
    class MyEventEmitter extends EventEmitter {}
    const myEventEmitter = new MyEventEmitter();
    myEventEmitter.on('ping'function() {
     console.log('pong');
    })
    myEventEmitter.emit('ping');

Stream 流

Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)

  • Stream 有四种流类型

    • Readable - 可读操作
    • Writable - 可写操作
    • Duplex - 可读可写操作
    • Transform - 操作被写入数据,然后读出结果
  • Stream 对象本身就是一个 EventEmitter ,常用事件有

    • data - 当有数据可读时触发。

    • end - 没有更多的数据可读时触发。

    • error - 在接收和写入过程中发生错误时触发。

    • finish - 所有数据已被写入到底层系统时触发。

  • Stream 内部含有 Buffer

  • Stream优势:只会先读文件需要的一部分,内存损耗较小

  • 详细api可查** **[nodejs.cn/api/stream.…][nodejs.cn_api_stream.]

    var readerStream = fs.createReadStream('input.txt',{start:50,end:99}); //读取一部分

常见全局对象

  • setTimeout 创建定时
  • clearTimeout(t) 停止定时
  • setInterval 创建轮询
  • clearInterval(t) 停止轮询
  • console 打印
  • process 进程

模块全局对象

模块加载时注入

  • __filename 模块文件的路径
  • __dirname 当前执行脚本所在的目录
  • exports 输出
  • module 模块
  • require 请求

npm(node package manager)

  • node.js 内置的用于安装和发布符合 node.js 标准的模块的⼀款⼯具,从⽽实现社区共建的⽬的繁荣整个社区。

  • npx 是 npm@5 之后新增的⼀个命令,它使得我们可以 在不安装模块到当前环境的前提下,使⽤⼀些 cli 功能,每次调用都会使用最新的版本。

    全局安装了 vue

    npm i -g vue vue init webpack test

    ⽆论是项⽬中还是全局都没有安装 vue (但实际上是安装了的,但表现确实像没有安装)

    npx vue test

发布一个npm包

  • 安装webpack简易框架(这里以发布vue插件为例)
    npm install -g @vue/cli-init //cli版本是3及以上,vue init 的运行效果将会跟 vue-cli@2.x 相同
    vue init webpack-simple marquee
    //安装完成目录结构
    文件目录/
    ├── index.html
    ├── package.json
    ├── README.md
    ├── .babelrc
    ├── .editorconfig
    ├── .gitignore
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   │   └── logo.png
    │   └── main.js
    └── webpack.config.js
  • 封装Vue插件(创建一个index.js)

    //在APP.vue中查看效果 npm install npm run dev

  • 在index.js中export封装好的Vue插件

  • 修改webpack.config.js

    const NODE_ENV = process.env.NODE_ENV;
    module.exports = {
      entry: NODE_ENV == 'development' ? './src/main.js' : './src/marquee/index.js',
      output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'marquee.js', //输出文件名
        library: 'marquee', // 指定的就是你使用require时的模块名
        libraryTarget: 'umd', // 指定输出格式, UMD 同时支持两种执行环境:node环境、浏览器环境。
        umdNamedDefine: true // 会对 UMD 的构建过程中的 AMD 模块进行命名。否则就使用匿名的 define
      },
    }
  • 打包
    npm run build
    //出现dist文件夹
  • 修改package.json
    {
     "author""maomincoding",  //author的值为npm用户名,这里一定要注意
      "main""dist/marquee.js", //main的值为刚才打包的路径文件
      "license""ISC", //license的值按照以上即可
      "keywords": ["marquee"], //keywords为用户搜索的关键词
      "private"false, //private设为false, 开源因此需要将这个字段改为 false
    }
  • 发包文件名单
    //npm发包默认包含的文件(不区分大小写)
    package.json
    README (and its variants)
    CHANGELOG (and its variants)
    LICENSE / LICENCE
    package.json属性main指向的文件
    //npm发包默认忽略的文件
    .git
    CVS
    .svn
    .hg
    .lock-wscript
    .wafpickle-N
    .*.swp
    .DS_Store
    ._*
    npm-debug.log
    .npmrc
    node_modules
    config.gypi
    *.orig
    package-lock.json (use shrinkwrap instead)
    //发包白名单,设置package.json中的files属性
    files:["package.json","src"]
    //发包黑名单,通过下面两个文件来设置忽略的文件或文件夹
    .gitignore
    .npmignore
    //文件设置优先级!!!
    files>.npmignore>.gitignore
  • 编辑README.md
  • npm包发布
    npm config get registry //查看登录源
    npm config set registry=http://registry.npmjs.org //如果不是http://registry.npmjs.org就切换一下
    npm login //登录 回车出现 Logged in as maomincoding on http://registry.npmjs.org,那么就成功了
    npm publish //成功!
  • npm包撤销 :只有在发包的24小时内才允许撤销,以后发包的时候也不能再和被撤销的包的名称和版本重复了,建议慎重! I sure hope you know what you are doing
    npm unpublish 包名 --force //撤销
  • npm包更新

    • 修复bug,小改动,c加1
    • 增加了新特性,但仍能向后兼容,b加1
    • 有很大的改动,无法向后兼容,a加1
    • 打开根目录下的package.json找到version字段 "version":"a.b.c"

    • 再次发布

nvm(node version manager)

管理 node 版本的⼀个⼯具,简单来说,就是通过将多个 node 版本安装在指定路径,然后通过 nvm 命令切换时,就会切换我们环境变量中 node 命令指定的实际执⾏的软件路径。

nrm(npm registry manager )

是npm的镜像源管理工具,使用这个可以快速地在 npm 源间切换。

服务端框架 express/koa

node.js 内部有⾮常多的内置模块,其中就有 http 模块,express/koa 实际上就是 对这个 http 模块的再封装,增加了中间件策略和其他 各种路由的通⽤处理,让我们写起来更加⽅便。

  • body-parser express处理body的中间件
  • cookie-parser express处理cookie的中间件
  • 中间件可以这样理解,对于需要多次书 写的业务逻辑,可以使⽤⼀种切⾯的形式,对相同逻辑进⾏通⽤处理。
  • **洋葱模型:**中间件线性的连贯的,自上而下(垂直)依次进行劫持。执行到next()会跳出当前继续向下一个中间件执行,结束后会返回之前中间件执行next()后面内容。
  • koa和express在同步场景下完全相同,处理异步时koa进行rosolve能正确执行回调顺序,但express缺少这个方法,会产生一些顺序问题。

周边工具简介

quickjs

quickjs 是⼀个 JS 的解析引擎,轻量代码量也不⼤,与之功能类似的就是 V8 引擎。
他最⼤的特点就是,⾮常⾮常轻量,这点从源码中也能提现,事实上并没有太多的代码,它的主要特点和优势:

  • 轻量⽽且易于嵌⼊:只需⼏个C⽂件,没有外部依赖,⼀个x86下的简单的“hello world”程序只要180KiB。
  • 具有极低启动时间的快速解释器:在⼀台单核的台式PC上,⼤约在100秒内运⾏ECMAScript 测试套件156000次。运⾏时实例的完整⽣命周期在不到300微秒的时间内完成。
  • ⼏乎完整实现ES2019⽀持,包括:模块,异步⽣成器和和完整Annex B⽀持 (传统的Web兼容性)。许多ES2020中带来的特性也依然会被⽀持。
  • 通过100%的ECMAScript Test Suite测试。
  • 可以将Javascript源编译为没有外部依赖的可执⾏⽂件。

deno

deno 是⼀类类似于 node.js 的 JS 运⾏时环境,同时他 也是由 node.js 之⽗⼀⼿打造出来的,他和 node.js ⽐ 有什么区别呢?

  • 相同点:

    • deno 也是基于 V8 ,上层封装⼀些系统级别的调⽤
    • 我们的 deno 应⽤也可以使⽤ JS 开发
  • 不同点:

    • deno 基于 rust 和 typescript 开发⼀些上层模块,所 以我们可以直接在 deno 应⽤中书写 ts
    • deno ⽀持从 url 加载模块,同时⽀持 top level await 等特性

sequelize ORM 框架

帮助我们抹平了 底层数据库的细节,我们使⽤这类框架,就能按照它的 语法进⾏书写,最终⽣成能够应⽤于各个平台的 sql 语句。

pm2 服务部署

使⽤ pm2 启动服务端、进⾏运维

    npm install -g pm2
    pm2 start ws-server.js —name my-server
    pm2 list
    pm2 monit
    pm2 logs ws-server.js

问几个问题

Q1:下面哪几种写法可以正确导出(commonJS)

    module.exports='hello word' //√
    exports.key='hello word' //√
    exports='hello word' //×

Q2:自测一下

    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');

script start->script end->promise1->promise2->setTimeout

Q3:再测一下

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

script start->async2 end->Promise->script end->async1 end->promise1->promise2->setTimeout

巨人的肩膀

  • [npm发布包以及更新包还有需要注意的几点问题(这里以发布vue插件为例)][npm_vue]
  • [你好,JavaScript异步编程---- 理解JavaScript异步的美妙][JavaScript_---- _JavaScript]
  • [这一次,彻底弄懂 JavaScript 执行机制][JavaScript]
  • [你不知道的 Event Loop][Event Loop]
  • [【THE LAST TIME】彻底吃透 JavaScript 执行机制][THE LAST TIME_ JavaScript]
  • [深入理解 JavaScript Event Loop][JavaScript Event Loop]
  • [从event loop规范探究javaScript异步及浏览器更新渲染时机][event loop_javaScript]
  • [一次弄懂Event Loop(彻底解决此类面试问题)][Event Loop 1]
  • [浏览器与Node的事件循环(Event Loop)有何区别?][Node_Event Loop]**


以上是关于一文搞懂Node.js以及浏览器中的事件循环机制的主要内容,如果未能解决你的问题,请参考以下文章

Node.js 的事件循环机制

一文搞懂Java/框架中的SPI机制

一文搞懂如何使用Node.js进行TCP网络通信

一文搞懂如何使用Node.js进行TCP网络通信

深入理解事件循环机制

彻底搞懂JS事件中的循环机制 Event Loop