Node.js VS 浏览器以及事件循环机制

微信扫一扫,分享到朋友圈

Node.js VS 浏览器以及事件循环机制

本文主要梳理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)是j s实现异步的一种方法 ,也是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.…
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 的核心就是事件触发与事件监听器功能的封装。

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.…
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包更新
    • 打开根目录下的package.json找到version字段 “version”:”a.b.c”
      • 修复bug,小改动,c加1
      • 增加了新特性,但仍能向后兼容,b加1
      • 有很大的改动,无法向后兼容,a加1

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' //×
复制代码

exports是module.exports值的引用,直接更改时引用地址进行了改变,不会对module.exports产生影响

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

巨人的肩膀

最后

欢迎纠错,看到会及时修改哒!

温故而知新,希望我们都可以保持本心,念念不忘,必有回响。

最后的最后

给自己的小组卖个安利,欢迎热爱前端的朋友们加入我们,一起学习,共同进步【有意请留言或私信,社畜搬砖不及时,但看到会立刻回复】 :heartpulse::heartpulse::heartpulse::heartpulse::heartpulse:

微信扫一扫,分享到朋友圈

Node.js VS 浏览器以及事件循环机制

Company Ghost Story 公司鬼故事 1

上一篇

MySQL慢查询语句分析总结

下一篇

你也可能喜欢

Node.js VS 浏览器以及事件循环机制

长按储存图像,分享给朋友