Node.js回调函数
什么是回调函数
function main(info, callback){//我是主函数,参数列表中的callback是一个回调函数
console.log('还在回家的路上');
console.log('到家了,发条信息吧');
callback (info);//调用回调函数
}
function sendMsg(msg){ //我是回调函数
console.log(msg);
}
main('亲爱的,我到家了!',sendMsg);//执行主函数
【注意】回调函数也可以通过一个匿名函数定义,回调函数一般放在函数参数列表的末尾,如:
- function f1(x,y,callback ) {… }
2. function f1(x,y,callback1,callback2 ) {... }
示例
- 使用node.js进行文件的读取。
两种方式:
同步方式,阻塞方式。
异步方式,边读边执行,基于回调函数,允许并行操作。
const fs = require("fs");//引入fs(filesystem)模块
//异步读取文件内容
fs.readFile('demo.txt', function (err, data) {
if (err) return console.error(err); //读取失败则报错
console.log(data.toString());//读取成功则输出文件内容
});
console.log("Node程序已经执行结束!");
结论:
不管文件是否读取并输出,都会继续执行。
文件读取完毕继续回调。
执行没有阻塞,不需要等待io。
事件机制
事件机制
应用采用事件进行驱动,在交互过程中会产生一些事件,单击、双击、拖动等,还有文件读取完毕或其他任务等,这些是按照顺序加入一个队列。node是单进程、单线程的,仍然支持高并发,是通过evenloop来实现,通过异步执行回调接口和事件驱动,node可以处理大量的并发事件,而且性能很高。
Node 的异步语法比浏览器更复杂,因为它可以跟内核对话,不得不搞了一个专门的库 libuv 做这件事。这个库负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行。
事件循环
事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去.只要用到引擎之外的功能,就需要跟外部交互,从而形成异步操作。由于异步操作实在太多,JavaScript 不得不提供很多异步语法。
既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到轮询 队列中等待时机执行。
e.g. : 网络故障诊断
当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本(或丢入 REPL),它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环。
事件循环注意
只有一个主线程,事件循环是在主线程上完成的。其次,Node开始执行脚本时,会先进行事件循环的初始化,但是此时事件循环还没有开始,会先完成以下工作:
同步任务
发出异步请求
规定定时器生效的时间
执行process.nextTick()等等
事件循环六个阶段
事件循环会无限次执行,一轮又一轮,只有异步任务的回调函数队列清空了,才会停止执行。每一轮的事件循环都分成六个阶段,这些阶段会依次执行。每一个阶段都有一个先进先出的回调函数队列,只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。
事件循环操作顺序
定时器
为了协调异步任务,Node 提供了四个定时器,让任务可以在指定的时间运行。
setTimeout()
setInterval()
setImmediate()
process.nextTick()
同步任务
同步任务即正常业务代码,同步任务总比异步任务任务先执行
异步任务
异步任务可以分成两种:
- 追加在本轮循环的异步任务
-
追加在次轮循环的异步任务
Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,同步任务执行结束完(优先级process.nextTick>promise.then()),即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
Timer
这个是定时器阶段,处理setTimeOut()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段,进行下一阶段。
I/O callbacks
除了以下操作的的回调函数,其他回调函数都在这个阶段执行。
setTimeOut()和setInterval()的回调函数
setImmediate()的回调函数
用于关闭请求的回调函数,比如socket.on(‘close’,…)
idle, prepare
该阶段只供 libuv 内部调用,这里可以忽略。
Poll
执行下限时间已经达到的timers的回调。
然后处理 poll 队列里的事件。
当event loop进入 poll 阶段,并且没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:
- 如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
- 如果 poll 队列为空
- 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里面的回调 callback)。
- 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。
- 当event loop进入 poll 阶段,并且有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。
check
该阶段执行setImmediate()的回调函数。
close callbacks
该阶段执行关闭请求的回调函数,比如socket.on(‘close’, …)。
事件循环例子
下面代码有两个异步任务,一个是 100ms 后执行的定时器,一个是文件读取,它的回调函数需要 200ms。请问运行结果是什么?
例子分析
第一轮事件循环: 没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。
第二轮事件循环: 依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。
第三轮事件循环: 已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒
setTimeout 和 setImmediate
由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成,请确定下面的执行结果。
Node.js事件的监听与触发
事件的监听与触发
Node.js不是在各个线程中为每个请求执行所有的任务,而是把任务添加到事件队列中,然后由一个单独的线程运行一个事件循环,将任务从事件队列中提取出来。事件循环获取事件队列中头部的任务,执行该任务,再找到下一个任务。当执行到长期运行或有阻塞I/O的代码时,它不是直接调用函数,而是将函数与一个要在此函数完成后执行的回调函数一起添加到事件队列中。当Node.js 事件队列中的所有事件都被完成时,Node.js应用程序终止。
EventEmitter类
EventEmitter类的核心就是事件触发与事件监听器功能的封装。所有能触发事件的对象都是EventEmitter类的实例。
EventEmitter对象的事件均由一个事件名和若干个参数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件,EventEmitter对象支持若干个事件监听器(Listener )。当事件触发(又称发射)时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。大多数Node.,js核心API基于异步事件驱动架构构建,其中某些类型的对象(又称触发器)会触发命名事件来调用函数(又称监听器)。
当EventEmitter对象触发一个事件时,所有绑定在该事件上的函数会被同步地调用。下面以门卫报告有人来了为例简单示范事件的监听与触发,eventEmitter.on()方法用于注册监听器(有人来就报告),eventEmitter.emit()方法用于触发事件(发现有人来了)。
EventEmitter类常用API
addListener(event, listener)
为指定事件添加一个监听器到监听器数组的尾部。
on(event, listener)
为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数。
once(event, listener)
为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发 后立刻解除该监听器。
removeListener(event, listener)
移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听
removeAllListeners([event])
移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器
setMaxListeners
setMaxListeners 函数用于改变监听器的默认限制的数量。
listeners(event)
返回指定事件的监听器数组。
emit(event, [arg1], [arg2], […])
按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false。
同一事件注册多个监听器
const EventEmitter = require('events').EventEmitter; // 加载事件模块
var event = new EventEmitter(); // 实例化事件模块
// 注册事件(seen)
event.on('seen', function(who) {
console.log('报告,来人是一位', who);
});
// 再次注册事件(seen)
event.on('seen', function() {
console.log('欢迎光临!');
});
event.emit('seen', '女士'); // 发射(触发)事件(seen)
总结
- 事件流程:引入模块 -> 实例化EventEmitter类 -> 注册事件 -> 触发事件
- Node.js中大部分的模块,都继承自Event模块,Event模块(events.EventEmitter)是一个简单的事件监听器模式的实现。具有addListener/on,once,removeListener,removeAllListeners,emit等基本的事件监听模式的方法实现。它与前端DOM树上的事件并不相同,因为它不存在冒泡,逐层捕获等属于DOM的事件行为,也没有preventDefault()、stopPropagation()、 stopImmediatePropagation() 等处理事件传递的方法。
全局对象
全局对象
- JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。
- 在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
- 在 Node.js 我们可以直接访问到 global 的属性,而不需要在应用中包含它。
全局对象和全局变量
global 最根本的作用是作为全局变量的宿主。按照 ECMAScript 的定义,满足以下条 件的变量是全局变量:
- 在最外层定义的变量;
- 全局对象的属性;
- 隐式定义的变量(未定义直接赋值的变量)。
全局变量__filename和__dirname
__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。
__dirname 表示当前执行脚本所在的目录。
console模块
process对象
process 是一个全局变量,即 global 对象的属性。它用于描述当前Node.js 进程状态的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候,就需要使用它,process 对象的一些最常用的成员包括以下几种:
对象事件
对象属性
对象方法
process对象事件
process.on('exit', function(code) {
// 以下代码永远不会执行
setTimeout(function(){
console.log("该代码不会执行");
}, 0);
console.log('退出码为:',code);
});
console.log("程序执行结束");
退出状态码
如果没有异步操作任务正在等待执行,则Node.js会以状态码0,无法阻止事件循环的退出,并且一旦exit监听器都完成了,node.js进程终止。
其他情形使用相应的状态码。(如:未捕获异常、内部错误、致命错误等)
process对象属性
获取process对象属性
process.stdout.write("Hello World!" + "\n");// 将字符串输出到终端
//通过参数读取
process.argv.forEach(function(val, index, array) {
console.log(index + ': ' + val);
});
console.log(process.execPath); // 获取执行路径
console.log(process.platform); // 获取平台信息
获取命令行参数
console.log(‘读取命令行参数:’, process.argv);
console.log(‘第1个参数:’, process.argv[2]);
process对象的方法
Node.js的定时器
设置定时器
一次性定时器
基本用法
setTimeout(callback, delay[, …args])
示例
setTimeout(function(){
console.log(‘我是一个一次性的定时器’);
},1000);
周期性定时器
基本用法
setInterval(callback, delay[, …args])
示例
setInterval (function(){
console.log(‘我是一个周期性的定时器’);
},1000);
即时定时器
基本用法
setImmediate(callback[, …args])
这个方法用于在IO事件的回调之后立即执行回调函数,其比上述setTimeout()方法少了一个delay参数,返回的是 Immediate对象。
这是一个即时定时器,该方法并不会立即执行回调函数,而是在事件轮询之后执行函数,为了防止轮询阻塞,在每轮循环中仅执行链表中的一个回调函数。当程序多次调用setlmmediate()方法时,由该参数指定的回调函数将按照创建它们的顺序排队等待执行。每次事件循环迭代都会处理整个回调队列。如果即时定时器通过正在执行的回调加入队列,则要等到下一次事件循环迭代时才会被触发。
取消定时器
分别用clearTimeout()、clearInterval()和clearImmediate()方法取消相应定时器,防止该定时器触发。
var testInterval=setInterval(testFunc,2000);
…
clearInterval(testInterval);
Timeout和Immediate类
Node.js内置两个有关定时器的类Timeout和Immediate,可用于创建相应的对象。
Timeout对象在内部创建,并由setTimeout()或setInterval()方法返回,可以传递给clearTimeout()或clearInterval()以取消定时器。
Immediate对象也在内部创建,并由setImmediate()方法返回。它可以传递给clearImmediate()以取消即时定时器。
setImmediate()方法与setTimeout()方法的对比
process.nextTick()在当前阶段立即执行。
setImmediate()在下一次迭代或事件循环的tick事件上被触发。
process.nextTick()的回调函数执行的优先级要高于setImmediate()。
在一个I/O周期(即主模块)内调用的比较
setTimeout(() => {
console.log('一次性');
}, 0);
setImmediate(() => {
console.log('即时性');
});
C:\nodeapp\ch02>node timeout_vs_immediate1.js
一次性
即时性
C:\nodeapp\ch02>node timeout_vs_immediate1.js
即时性
一次性
同一个I/O循环内调用的比较
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('一次性');
}, 0);
setImmediate(() => {
console.log('即时性');
});
});
C:\nodeapp\ch02>node timeout_vs_immediate2.js
即时性
一次性
C:\nodeapp\ch02>node timeout_vs_immediate2.js
即时性
一次性
process.nextTick()与setImmediate()的对比
process.nextTick()在当前阶段立即执行。
setImmediate()在下一次迭代或事件循环的tick事件上被触发。
process.nextTick()的回调函数执行的优先级要高于setImmediate()。
console.log('开始');
process.nextTick(() => {
console.log('下一个时间点的回调');
});
console.log('调度');
结果:
开始、调度、下一个时间点的回调
Buffer类型
可以将Buffer视为一种用来处理二进制数据的数据类型。
Buffer类的实例(即对象)类似于整数数组,但实例对应于固定大小的原始内存分配,其大小在创建时被确定且无法更改。
创建Buffer实例
Buffer实例创建使用Buffer.from()、Buffer.alloc()或Buffer.allocUnsafe()方法。
// 创建一个包含数组[0x1, 0x2, 0x3]的Buffer实例
const buf1 = Buffer.from([1, 2, 3]);
// 创建一个包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer实例
const buf2 = Buffer.from('tést');
// 创建一个包含 Latin-1(说明见2.6.2节)字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer实例
const buf3 = Buffer.from('tést', 'latin1');
// 创建一个长度为 10、且用零填充的 Buffer实例
const buf4 = Buffer.alloc(10);
// 创建一个长度为 10、且用 0x1 填充的 Buffer实例
const buf5 = Buffer.alloc(10, 1);
/* 创建一个长度为 10、且未初始化的 Buffer实例。这个方法比调用 Buffer.alloc()更快,
但返回的 Buffer 实例可能包含旧数据,因此需要使用 fill() 或 write() 重写。*/
const buf6 = Buffer.allocUnsafe(10);
Buffer用于编码转换
Buffer实例一般用于表示编码字符的序列,如UTF-8、UCS2、Base64或十六进制编码的数据。
在文件操作和网络操作中,如果没有显式声明编码格式,返回数据的默认类型为Buffer。
通过使用显式字符编码将Buffer实例与JavaScript字符串相互转换。
在创建Buffer实例时指定存入字符串的字符编码
const buf = Buffer.from('hello world', 'ascii');
将已创建的Buffer实例转换成字符串的用法
buf.toString([encoding[, start[, end]]])
示例
const buf = Buffer.from('tést');
console.log(buf.toString('hex'));// 输出结果: 74c3a97374
console.log(buf.toString('utf8', 0, 3));//输出结果:té
将Buffer实例转换为JSON对象
使用buf.toJSON()方法将Buffer实例转换为JSON对象,适用于将二进制数据转换为JSON格式。
示例
const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]);
const json = JSON.stringify(buf);
console.log(json); // 输出:{"type":"Buffer","data":[1,2,3,4,5]}
const copy = JSON.parse(json, (key, value) => {
return value && value.type === 'Buffer' ?
Buffer.from(value.data) :
value;
});
console.log(copy); // 输出: <Buffer 01 02 03 04 05>
写入Buffer实例
使用buf.write()方法将字符串写入Buffer实例
buf.write(string[, offset[, length]][, encoding])
示例
const buf = Buffer.alloc(256);
const len = buf.write('\u00bd + \u00bc = \u00be', 0);
console.log(`${len} 个字节: ${buf.toString('utf8', 0, len)}`);
// 输出: 12 个字节: ½ + ¼ = ¾
从Buffer实例读取数据
使用buf.toString()方法从Buffer实例读取字符串
使用其他专用方法从Buffer实例读取其他类型的数据
const buf = Buffer.from([-1, 5]);
console.log(buf.readInt8(0));// 输出结果: -1
console.log(buf.readInt8(1));// 输出结果: 5
console.log(buf.readInt8(2));// 抛出异常 ERR_OUT_OF_RANGE(超出范围)
Buffer实例合并
使用Buffer.concat()方法
Buffer.concat(list[, totalLength])
Buffer实例复制
使用buf.copy()方法
buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
Buffer实例切片
使用buf.slice()方法
buf.slice([start[, end]])