学习目标:
宏任务与微任务
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);
结果为: