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

一文搞懂Node.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’ //×
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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值