一文搞懂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 的运行效果将会跟 [email protected] 相同
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]**