Node.js中的宏任务与微任务、node.js架构组成、事件循环、模拟事件、fs模块

学习目标:
        宏任务与微任务
        nodejs架构组成
        事件循环
        模拟事件
        fs模块

宏任务与微任务

 介绍

宏任务与微任务表示异步任务的两种分类
        宏任务:包括整体代码script(可以理解为外层同步代码)、settimeout、
        setInterval、i/o、ui render、异步ajax、文件操作等

        微任务:promise、Object.observe(用来实时监测js中对象的变化)、
        MutationObserver(监听DOM树的变化)

因为异步任务放在队列中,自然而然宏任务与微任务就存放在宏任务队列与微任务队列中

执行顺序

先执行主线程执行栈中的代码(同步任务),那异步任务怎样执行的?
先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微
任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微
任务从队列中调入主线程执行,微任务执行完毕后。再将异步宏任务从队列中调入
主线程执行,一直循环直至所有任务执行完毕(事件循环EventLoop)。
        每一个宏任务执行完之后,都会检查是否存在待执行的微任务
        如果有,则执行完所有微任务之后,再继续执行下一个宏任务

练习题
题1

//setTimeout第二个参数可以省略,默认为0
setTimeout(function () {
console.log('1');
})
new Promise(function (resolve) {
console.log('2');
resolve();
}).then(function () {
console.log('3');
})
console.log('4');
//打印顺序 2 4 3 1

执行顺序:
1. 遇到setTimeout,异步宏任务将其放到宏任务列表中 命名为time1;
2. new Promise 在实例化过程中所执行的代码都是同步执行的

(function中的代码),输出2;
3. 将Promise中注册的回调函数放到微任务队列中,命名为then1;
4. 执行同步任务console...log('4') ,输出4,至此执行栈中的代码执行完毕;
5. 从微任务队列取出任务then1到主线程中,输出3,至此微任务队列为空;
6. 从宏任务队列中取出任务time1到主线程中,输出1,至此宏任务队列为空

题2

console.log(1);
setTimeout(function () {
console.log(2);
let promise = new Promise(function (resolve, reject) {
console.log(3);
resolve();
}).then(function () {
console.log(4);
});
}, 1000);
setTimeout(function () {
console.log(5);
let promise = new Promise(function (resolve, reject) {
console.log(6);
resolve();
}).then(function () {
console.log(7);
});
}, 0);
let promise = new Promise(function (resolve, reject) {
console.log(8);
resolve()
}).then(function () {
console.log(9);
}).then(function () {
console.log(10)
});
console.log(11);
//1 8 11 9 10 5 6 7 2 3 4

执行顺序:
1. 执行同步任务console.log(1) ,输出1;
2. 遇到setTimeout放到宏任务队列中,命名time1;
3. 遇到setTimeout放到宏任务队列中,命名time2;
4. new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代
码),输出8;
5. 将Promise中注册的回调函数放到微任务队列中,命名为then1;
6. 将Promise中注册的回调函数放到微任务队列中,命名为then2;
7. 执行同步任务console.log(11)输出11;
8. 从微任务队列取出任务then1到主线程中,输出9;
9. 从微任务队列取出任务then2到主线程中,输出10,至此微任务队列为空;
10. 从宏任务队列中取出time2(注意这里不是time1的原因是time2的执行时间为
0);
11. 执行同步任务console.log(5),输出5;
12. new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代
码),输出6;

13. 将Promise中注册的回调函数放到微任务队列中,命名为then3,至此宏任务
time2执行完成;
14. 从微任务队列取出任务then3到主线程中,输出7,至此微任务队列为空;
15. 从宏任务队列中取出time1,至此宏任务队列为空;
16. 执行同步任务console.log(2),输出2;
17. new Promise 在实例化过程中所执行的代码都是同步执行的(function中的代
码),输出3;
18. 将Promise中注册的回调函数放到微任务队列中,命名为then4,至此宏任务
time1执行完成;
19. 从微任务队列取出任务then4到主线程中,输出4,至此微任务队列为空。

NodeJS架构组成

Node.js是一个构建在Chrome浏览器 V8引擎上的JavaScript运行环境,使用 单线程、
事件驱动、非阻塞I/O 的方式实现了高并发请求, libuv 为其提供了异步编程的能
力。
Node.js标准库:由JavaScript编写的,也就是我们使用过程中直接能调用的API,在
源码中的 lib目录下可以看到,诸如 http、fs、events 等常用核心模块。
Node bindings: 可以理解为是JavaScript与C/C++库之间建立连接的桥 ,通过这个
桥,底层实现的C/C++库暴露给JavaScript环境,同时把 js传入V8 , 解析后交给 libuv
发起非阻塞 I/O , 并等待事件循环调度;
V8: Google推出的Javascript虚拟机,为Javascript提供了在非浏览器端运行的环
境;

libuv:为Node.js提供了跨平台,线程池,事件池,异步I/O 等能力,是Nodejs之所
以高效的主要原因;

C-ares:提供了异步处理DNS相关的能力;
http_parser、OpenSSL、zlib等:提供包括http解析、SSL、数据压缩等能力

 1.1 单线程

任务调度一般有两种方案:
一是单线程串行执行 ,执行顺序与编码顺序一致,最大的问题是无法充分利用多核
CPU,当并行极大的时候,单核CPU理论上计算能力是100%;
另一种就是多线程并行处理,优点是可以有效利用多核CPU,缺点是创建与切换线
程开销大,还涉及到锁、状态同步等问题,CPU经常会等待I/O结束,CPU的性能就
白白消耗。
通常为客户端连接创建一个线程需要消耗2M内存,所以理论上一台8G的服务器,
在Java应用中最多支持的并发数是4000。而Node.js只使用一个线程,当有客户端连
接请求时,触发内部事件,通过非阻塞I/O,事件驱动机制,让其看起来是并行
的。理论上一台8G内存的服务器,可以同时容纳3到4万用户的连接。Node.js采用
单线程方案,免去锁、状态同步等繁杂问题,又能提高CPU利用率。Node.js的高效
除了因为其单线程外,还必须配合非阻塞I/O。

1.2 非阻塞I/O

I/O就是input/output,一个系统的输入输出。

阻塞I/O和非阻塞I/O的区别就在于系统的接收输入,在到输出期间,能不能接收其他输入。
非阻塞I/O让我们减少了许多等待的时间,并且在等待时间内,我们还可以进行一些其他的操作。

发起I/O操作不等得到响应或者超时就立即返回,让进程继续执行其他操作,但是要
通过轮询方式不断地去check数据是否已准备好。Java属于阻塞IO,即在发起I/O操
作之后会一直阻塞着进程,不执行其他操作,直到得到响应或者超时为止;
Node.js中采用了非阻塞型I/O机制,因此在执行了读数据的代码之后,将立即转而
执行其后面的代码,把读数据返回结果的处理代码放在回调函数中,从而提高了程
序的执行效率。当某个 I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,
线程执行这个事件的回调函数。
例如:当你的 JavaScript 程序发出了一个 Ajax 请求(异步)去服务器获取数据,在回
调函数中写了相关 response 的处理代码。 JavaScript 引擎就会告诉宿主环境:
“嘿,我现在要暂停执行了,但是当你完成了这个网络请求,并且获取到数据的时
候,请回来调用这个函数”。然后宿主环境(浏览器)设置对网络响应的监听,当返回
时,它将会把回调函数插入到事件循环队列里然后执行

2 事件循环

2.1 基本流程

1. 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈 (execution
context stack);
2. 主线程之外,还维护一个事件队列 (Event queue),当用户的网络请求或者其
它的异步操作到来时,会先进入到事件队列中排队,并不会立即执行它,代码
也不会被阻塞,继续往下走,直到主线程代码执行完毕;
3. 主线程代码执行完毕完成后,然后通过事件循环机制 (Event Loop),检查队列
中是否有要处理的事件,从队头取出第一个事件,从线程池分配一个线程来处理这个事件,然后是第二个,第三个,直到队列中所有事件都执行完了。当有事件执行完毕后,会通知主线程, 主线程执行回调,并将线程归还给线程 池。这个过程就叫事件循环 (Event Loop);
4. 不断重复上面的第三步

2.2 事件循环的6个阶段

事件循环是一个循环体,在循环体中有6个阶段,在每个阶段中,都有一个事件队
列,不同的事件队列存储了不同类型的异步API的回调函数。
事件循环在每次执行的时候,都有6个阶段的事情要做。

 

 

 poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里。v8引擎将js代码解
析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看
poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为
空时,会检查是否有 setImmediate()的callback,如果有就进入check阶段执行这些
callback。但同时也会检查是 否有到期的timer,如果有,就把这些到期的timer的
callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中
的 callback。 这两者的顺序是不固定的,受到代码运行的环境的影响。如果两者的
queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入
i/o callback阶段并立即执行这个事件的 callback。

Event Loop 从任务队列获取任务,然后将任务添加到执行栈中( 动态,根据函
数调用),JavaScript 引擎获取执行栈最顶层元素(即正在运行的执行上下文)进
行运行!
整体过程:定时器检测阶段-->I/O阶段-->闲置阶段-->轮询阶段-->检查阶段-->关闭事
件回调阶段-->定时器检测阶段......

2.3 宏任务

Event Loop 有一个或多个任务(宏任务)队列,当 执行栈 为空时,会从任务队列里获
取任务,加入到执行栈中,这里的任务就是宏任务。如下都是宏任务:
Events---事件交互
Callbacks---回调函数
Using a resource(I/O)----使用资源,包括ajax
Reacting to DOM manipulation---对DOM操作的反应
script(整体script也被看做是一个宏任务,所以一开始整个script脚本都被推入
宏任务中,事件循环是从第一个宏任务开始的)
setTimeout()/setInterval()/setImmediate()---定时器
requestAnimationFrame()---制作动画的一个函数

2.4 微任务

每个 Event Loop 有一个微任务队列,同时有一个 microtask checkpoint ,即每执行
完成一个宏任务后,就会 check 微任务。所以,当某个宏任务执行完成后,会先执
行微任务队列,执行完成后,再次获取新的宏任务。这里微任务相当于插队操作!!
如下都是微任务:
process.nextTick()
Promise(aync/await)
Object.observe(已过期,是监听对象变化的一个函数)
MutationObserver(监听dom树变化)

练习题

// 事件循环、轮询
console.log('begin');
setTimeout(() => {
console.log('timeout');
}, 0)
new Promise((resolve, reject) => {
console.log('promise');
resolve();
}).then(() => {
console.log('resolve');
})
console.log('end');
/*结果为:
begin
promise
end
resolve
timeout
*/

2.5 process.nextTick()

当将一个函数传给 process.nextTick() 时,则指示引擎在当前操作结束(在下
一个事件循环滴答开始之前)时调用此函数 ,意味着插队。所以,当要确保在下一
个事件循环迭代中代码已被执行,则使用 process.nextTick() 。

console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0)
Promise.resolve().then(() => {
console.log('promise');
})
process.nextTick(() => {
console.log('nextTick callback');
});
console.log('scheduled');
// start
// scheduled
// nextTick callback
// promise
// timeout

2.6 setImmediate()

作为 setImmediate() 参数传入的任何函数都是在事件循环的下一个迭代中执行的回
调。
需要注意的是:传给 process.nextTick() 的函数会在事件循环的当前迭代中(当前操
作结束之后)被执行, 这意味着它会始终在 setTimeout 和 setImmediate 之前
执行;
延迟 0 毫秒的 setTimeout() 回调与 setImmediate() 非常相似。
执行顺序取决于各种因素,但是它们都会在事件循环的下一个迭代中运行

console.log('start');
setTimeout(() => {
console.log('timeout');
}, 100)
process.nextTick(() => {
console.log('nextTick callback');
});
Promise.resolve().then(() => {
console.log('promise');
})
setImmediate(() => {
console.log('setImmediate');
})
console.log('scheduled');
// start
// scheduled
// nextTick callback
// promise
// setImmediate
// timeout

执行顺序:执行栈-->process.nextTick()-->任务队列-->setImmediate()

console.log('1');
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
)
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
// 1 7 6 8 2 4 3 5 9 11 10 12

3 模拟事件

Node.js 的大部分核心 API 都是围绕惯用的异步事件驱动架构构建的,在该架构
中,某些类型的对象(称为"触发器")触发命名事件,使Function对象("监听器")被调
用。

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');

3.1 API

emitter.on(eventName, listener)

事件注册

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 注册事件
myEmitter.on('event', (param) => {
console.log('an event occurred!', param);
});

emitter.addListener(eventName, listener)

事件注册,emitter.on(eventName, listener) 的别名

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 注册事件
myEmitter.addListener('event2', (param) => {
console.log('an event2 occurred!', param);
});

emitter.once(eventName, listener)

事件注册一次,该监听器最多为特定事件调用一次。 一旦事件被触发,则监听器就
会被注销然后被调用。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 注册只执行一次的事件
myEmitter.once('event3', (param) => {
console.log('an event3 occurred!', param);
});

emitter.emit(eventName[,param,param])

事件发射器,eventName表示模拟事件名,param为传递的参数。

myEmitter.emit('event', '我是参数');
myEmitter.emit('event', '我是参数');
myEmitter.emit('event2', '我是参数2');
myEmitter.emit('event2', '我是参数2');
myEmitter.emit('event3', '我是参数3');
myEmitter.emit('event3', '我是参数3');

运行结果如下:

an event occurred! 我是参数
an event occurred! 我是参数
an event2 occurred! 我是参数2
an event2 occurred! 我是参数2
an event3 occurred! 我是参数3 //这个event3是使用once声明的事件

emitter.eventNames()

返回列出触发器已为其注册监听器的事件的数组。 数组中的值是字符串或 Symbol

console.log(myEmitter.eventNames()); //[ 'event', 'event2',
'event3' ]
myEmitter.emit('event', '我是参数');
myEmitter.emit('event', '我是参数');
myEmitter.emit('event2', '我是参数2');
myEmitter.emit('event2', '我是参数2');
myEmitter.emit('event3', '我是参数3');
myEmitter.emit('event3', '我是参数3');
console.log(myEmitter.eventNames()); //[ 'event', 'event2' ]

emitter.listeners(eventName)

返回名为 eventName 的事件的监听器数组的副本。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
function myListener(param) {
console.log('an event4 myListener occurred!', param);
}
function myListener2(param) {
console.log('an event4 myListener2 occurred!', param);
}
myEmitter.addListener('event4', myListener)
console.log(myEmitter.listeners('event4')); //结果: [ [Function:
myListener] ]
myEmitter.addListener('event4', myListener2)
console.log(myEmitter.listeners('event4')); //结果:[ [Function:
myListener], [Function: myListener2] ]
// 如果是匿名函数,则返回:[ [Function (anonymous)] ]

emitter.off(eventName, listener)

事件解绑

myEmitter.off('event4', myListener)
console.log(myEmitter.listeners('event4')); //结果:[ [Function:
myListener2] ]

emitter.removeListener(eventName, listener)

事件解绑

myEmitter.removeListener('event4', myListener2)
console.log(myEmitter.listeners('event4')); //结果:[]

emitter.removeAllListeners([eventName])

事件全部解绑

console.log(myEmitter.listeners('event4')); //结果:[ [Function:
myListener], [Function: myListener2] ]
myEmitter.removeAllListeners('event4'); //结果:[]

3.2 模拟事件机制

/**
* 事件机制
*/
class EventEmitter {
constructor() {
this.listeners = {}; // 存放事件监听函数{ "event1": [f1,f2,f3],
"event2":[f1,f2] }
}
// 在jq中,一个事件源可以绑定多个事件处理函数,同一个事件可以绑定多个事件处
理程序
addEventListener(eventName, handler) {
let listeners = this.listeners;
// 先判断这个如果这个事件类型对应的值是一个数组,说明其中已经绑定过了事件
处理
// 如果该事件处理函数未绑定过,进行绑定
if (listeners[eventName] instanceof Array) {
if (listeners[eventName].indexOf(handler) === -1) {
listeners[eventName].push(handler);
}
} else {
// 否则,完成该事件类型的首次绑定
listeners[eventName] = [].concat(handler);
}
}
// 移除事件处理函数
removeEventListener(eventName, handler) {
let listeners = this.listeners;
let arr = listeners[eventName] || []; // 找到该事件处理函数在数组
中的位置
let i = arr.indexOf(handler);
// 如果存在,则删除
if (i >= 0) {
listeners[eventName].splice(i, 1);
}
}
// 派发事件,本质上就是调用事件
dispatch(eventName, params) {
this.listeners[eventName].forEach(handler => {
handler.apply(null, params);
});
}
}

4 fs模块

fs 是 filesystem 的缩写,该模块提供本地文件的读写能力,这个模块几乎对
所有操作提供异步和同步两种操作方式,供开发者选择。
准备工作:ReadMe.txt文件内容如下:

4.1 readFile()

异步读取数据。该方法的第一个参数是文件的路径,可以是绝对路径,也可以是相
对路径。 注意,如果是相对路径,是相对于当前进程所在的路径。第二个参数是读
取完成后的回调函数。 该函数的第一个参数是发生错误时的错误对象,第二个参数
是代表文件内容的 Buffer 实例。

//当前文件名是test.js
const fs = require('fs');
fs.readFile('./ReadMe.txt', function (err, buffer) {
if (err) throw err;
console.log(buffer);
});
console.log('程序执行完毕');

运行结果:

 4.2 readFileSync()

用于同步读取文件,返回一个Buffer实例。方法的第一个参数是文件路径,第二个
参数可以是 一个表示配置的对象,也可以是一个表示文本文件编码的字符串。默认
的配置对象是 {encoding: null, flag: 'r'} ,即文件编码默认为 null ,读
取模式默认为 r (只读)。
如果没有设置字符编码,读取的是Buffer流。

const fs = require('fs');
let fileName = './ReadMe.txt';
let text = fs.readFileSync(fileName, 'utf8');
console.log(text);
// 将文件按行拆成数组 ?代表匹配0次或者1次
text.split(/\r?\n/).forEach(function (line) {
// ...
console.log(line);
});

运行结果:

 4.3 writeFile()

用于异步写入文件。该方法的第一个参数是写入的文件名,第二个参数是写入的字
符串,第三个参数是回调函数。回调函数前面,还可以再加一个参数,表示写入字
符串的编码(默认是utf8)

const fs = require('fs');
let fileName = './ReadMe.txt';
fs.writeFile(fileName, 'Hello Node.js', (err) => {
if (err) throw err;
console.log('It\'s saved!');
});

结果:ReadMe.txt文件中原来的内容不再保留,保留新内容。

 4.4 writeFileSync()

该方法用于同步写入文件

const fs = require('fs');
let fileName = './ReadMe.txt';
fs.writeFileSync(fileName, "我是新内容", 'utf8');
console.log('It\'s saved!');

结果:ReadMe.txt文件中原来的内容不再保留,保留新内容。

 4.5 exists(path, callback)

通过检查文件系统来测试给定的路径是否存在,在新版本中已被废弃。建议使用
stat()或者access()
然后使用 true 或 false 调用 callback 参数:

const { exists } = require('fs');
exists('./ReadMe.txt', (e) => {
console.log(e); //e为true或者false,目前为true
console.log(e ? 'it exists' : 'no passwd!'); //it exists
});

4.6 stat()

可用来判断文件是否存在

const { stat } = require('fs');
stat('./ReadMe.txt', (err, stats) => {
console.log(!err ? 'it exists' : 'no passwd!');
});

4.7 access()

可用来判断文件是否存在

const { access } = require('fs');
access('./ReadMe.txt', (err) => {
console.log(!err ? 'it exists' : 'no passwd!');
});

4.8 mkdir(path[, options], callback)

异步地创建目录。

const { mkdir } = require('fs');
mkdir('./a', (err) => {
if (err) throw err;
console.log('success');
});

4.9 mkdirSync(path[, options])

同步地创建目录。 返回 undefined 或创建的第一个目录路径(如果 recursive
为 true )。 这是 fs.mkdir()fs.mkdir() 的同步版本。

const { mkdirSync } = require('fs');
let result = mkdirSync('./c/b', { recursive: true });
console.log(result); //结果:./c

 4.10 readdir(path[, options], callback)

读取目录的内容。 回调有两个参数 (err, files) ,其中 files 是目录中文件
名的数组,不包括 '.' 和 '..' 。

const { readdir } = require('fs');
readdir('./day01', (err, files) => {
if (err) throw err;
console.log(files);
});

结果为:

 4.11 readdirSync(path[, options])

同步读取目录的内容。

const { readdirSync } = require('fs');
let result = readdirSync('./day01');
console.log(result);

结果为:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值