第一章 Node核心

112 阅读26分钟

Node概述

Node是JavaScript的一个运行环境

image.png

image.png

浏览器仅为JavaScript提供了有限的功能,因此JS只能使用浏览器提供的功能实现有限的操作

NodeJS提供了完整的控制计算机的能力,通过使用Node提供的接口,可以实现对整个操作系统的控制

image.png

Node可以用于开发桌面应用程序,也可以用于开发服务器程序

当使用Node开发服务器程序时,通过会是以下两种形式:

  1. 单独使用Node完成服务器的开发,即Node需要完成请求的处理、响应、数据交互、以及各种业务逻辑

    这种结构通常应用在小型的网站上

    image.png

  2. 使用Node开发中间服务器,中间服务器需要完成与业务逻辑无关的事情,通常是进行请求和响应的转发,将请求转发给真正用于业务逻辑处理的服务器(通常采用其他后端语言编写而成),将其他服务器的响应内容转发给浏览器

    中间服务器除了可以对请求进行转发,也可以做一些简单的工作,例如:记录用户偏好、保存静态资源等

    image.png

全局对象

Node的全局对象叫做global,global中的所有属性,都可以直接使用

global中主要包含了以下属性:

  1. setTimeout、setInterval、clearTimeout、clearInterval

    Node中的计时器与JS中的计时器功能完全相同,但Node中的计时器返回的不是一个数字,而是一个对象,清除计时器时,也应该传入计时器所返回的对象

    Node中的计时器中设置的计时时间最少也会是1ms,即使你设置其为0

  2. setImmediate、clearImmediate

    用于“立即”执行某个回调函数

    setTimeout(()=>{}, 0)的执行顺序是不确定的

  3. console

    同JS中的console

  4. Buffer

    Buffer继承自Uint8Array

    对buffer使用toString("utf-8")可以得到每个数组元素在unicode码表中的字符形式

    const buf = Buffer.from([97, 98, 99, 100]);
    
    console.log(buf.toString("utf-8"));			// "abcd"
    
  5. process

    process.cwd():得到运行node命令时的终端中显示的命令行路径

    process.exit():强制退出node进程

    process.argv:获取执行node命令时命令中的所有参数

    process.platform:获取当前的操作系统的平台名称

    process.kill(pid):杀死进程id为pid的进程

    process.env:获取系统环境变量

Node的模块化细节

模块的查找

在Node中,使用require导入其他模块,无论require中的路径是绝对路径还是相对路径,最终都会被转换为绝对路径

使用相对路径导入模块时,./../都是相对于当前模块,且相对路径最终会被转换为绝对路径

还有一种相对路径是直接书写模块名(require("lodash")),对于这种路径,node会按照下面的顺序查找模块:

  1. 检查导入的模块是否是内置模块
  2. 检查当前文件所在目录中的node_modules目录
  3. 返回上一层目录,查找该目录中的node_modules目录

如果找到了,就加载模块并执行模块代码;如果到达根盘符了都没有找到,则报错(Cannot find module "xxx"

关于后缀名,如果模块路径中没有书写后缀名,则node会先查找无后缀的文件,如果不存在,node就会自动补全后缀名,补全的后缀名包括:.js、.json、.node,这三个后缀名从左到右优先级递减(有.js就忽略.json)

导入的如果是json文件,则文件中的内容会作为require函数的返回值,返回时node会自动将json转换为js对象

如果上面的同名文件都不存在,则尝试查找同名目录,如果目录是一个包,且包含了所需的入口文件,则导入该目录实际上是导入该包的入口文件;如果目录不是一个包,或者包中没有对应的入口文件,则导入该目录时会依次查找目录下的index.js、index.json、index.node文件

module对象

在Node的全局环境下,包含一个名称为module的对象,该对象中包含了以下属性:

  • exports

    对象,用于导出数据

  • id

    字符串

    对于node命令中的启动模块,id为"."

    对于其它模块,id为模块的绝对路径

  • parent

    父模块的module对象

    谁导入的该模块,谁就是该模块的父模块

  • filename

    字符串

    模块的绝对路径

  • children

    子模块的module对象

    模块中导入了哪些模块,导入的模块就是该模块的子模块

  • path

    字符串

    模块所在目录的绝对路径

  • paths

    被寻找的node_modules目录的数组

    Node就是根据根据该属性来查找模块的

require函数

在Node模块的全局,包含一个名称为require的函数,它包含了以下属性:

  • resolve

    函数,该函数可以将传入进去的路径转换为绝对路径

  • main

    启动模块的module对象

    node命令中使用的模块就是启动模块

  • extensions

    对象,对象中包含了不同后缀名的处理函数

    使用require导入不同后缀名的文件时,实际上是交给require的extensions中相应处理函数进行处理的

    例如:导入.json模块时,extensions中的处理.json文件的函数就会将文件内容解析为普通的js对象然后返回

  • cache

    已缓存的模块的模块module对象

注意:module对象和require函数都可以在模块的“全局”下使用,但它们并不是全局对象global上的属性

require函数的实现原理

// 伪代码

function require(modulePath){
    // 将传入的路径转换为完整的绝对路径
    modulePath = require.resolve(modulePath);
    
    // 判断模块是否已经缓存
    if(require.cache[modulePath]){
        return require.cache[modulePath];
    }
    
    // 读取文件内容(js代码),并将文件内容放入一个临时函数
    function __temp(module, exports, require, __dirname, __filename){
        // 导入的模块中的内容(js代码)
        ...
    }

	const module = {
        exports: {},
        ...
    };
	const exports = module.exports;

	__temp.call(exports, module, exports, require, module.path, module.filename);

    // 缓存模块内容
	require.cache[modulePath] = module.exports;

	return module.exports;
}

传递给require函数中的相对路径都是相对于require所在的模块的

除了require函数外,其它所有函数,传递进去的相对路径参数,都是相对于执行命令的命令行所在目录(即相对于process.cwd())

__dirname代表当前运行的模块所在目录的绝对路径,__filename代表当前运行的模块的绝对路径

Node中的ES模块化

Node中的模块要么全部使用commonjs模块化,要么全部使用ES模块化,现在暂时还不支持模块化混用

要在Node中使用ES模块化,需要将模块后缀名改为.mjs,或者将package.json中的type字段修改为"module"

基本内置模块

os模块

  • EOL

    end-of-line,行结束符

    不同操作系统的行结束符有所差异,使用该属性可以得到本系统所使用的行结束符

  • arch()

    获取系统架构名

  • cpus()

    获取主机中所有cpu的信息

  • freemem()

    获取当前空闲的内存大小,以字节为单位

  • homedir()

    得到主机的用户目录的绝对路径

  • hostname()

    获取主机名

  • tmpdir()

    获取主机的临时目录的绝对路径

path模块

  • basename(path)

    获取path中的文件名部分

  • sep

    获取系统的路径分隔符

  • delimiter

    获取系统环境变量的分隔符

  • dirname(path)

    获取path中除文件名外的剩余部分

  • extname(path)

    获取path中文件名的后缀名

  • join(path1, path2, ...)

    将多段路径拼接成为一个完整的路径

  • normalize(path)

    规范化路径

  • relative(path1, path2)

    返回path2相对于path1的相对路径

  • resolve(path1, path2, ...)

    将多段路径进行拼接成为一个完整的绝对路径

url模块

该内置模块会返回一个对象,对象中包含一个构造函数URL,URL可以将传递进去的url字符串进行分析,并将分析结果加入到URL实例之中

const URL = require("url");

const url = new URL.URL("https://www.baidu.com:443/a/b/c.html?a=1&b=2#hash");

/*
    URL {
		href: 'https://www.baidu.com/a/b/c.html?a=1&b=2#hash',
		origin: 'https://www.baidu.com',
		protocol: 'https:',
		host: 'www.baidu.com:443',
		hostname: 'www.baidu.com',
		port: '443',
		pathname: '/a/b/c.html',
		search: '?a=1&b=2',
		searchParams: URLSearchParams { 'a' => '1', 'b' => '2' },
		hash: '#hash'
    }
*/

url模块所导出的内容,其中包含一个parse方法,调用该方法基本等价于调用了URL构造函数

const URL = require("url");

const url = URL.parse("https://www.baidu.com:443/a/b/c.html?a=1&b=2#hash");

/*
    Url {
        protocol: 'https:',
        slashes: true,
        auth: null,
        host: 'www.baidu.com:443',
        port: '443',
        hostname: 'www.baidu.com',
        hash: '#hash',
        search: '?a=1&b=2',
        query: 'a=1&b=2',
        pathname: '/a/b/c.html',
        path: '/a/b/c.html?a=1&b=2',
        href: 'https://www.baidu.com:443/a/b/c.html?a=1&b=2#hash'
    }
*/

URL实例中包含一个属性searchParams,它是一个类似于Map的对象,其中的属性来自于对url的query部分进行解析的结果

searchParams对象中包含了has和get方法,用于判断是否存在某个query以及获取某个query的值

console.log(url.searchParams.has("c"));		// false
console.log(url.searchParams.get("a"));		// '1'

如果存在一个类似于URL实例的对象,想要根据该对象得到原来的url,可以使用该模块对象中的format方法

const URL = require("url");

const obj = {
    origin: "https://www.baidu.com:443",
    protocol: "https",
    host: "www.baidu.com",
    ...
};

const url = URL.format(obj);			// url = "https://www.baidu.com:443..."

util模块

  • callbackify(fn)

    将异步函数转换为回调的形式

    该方法会返回一个高阶函数,高阶函数就是采用回调的形式

  • inherits(Son, Parent)

    让Son继承自Parent

  • isDeepStrictEqual(data1, data2)

    深度比较两个数据是否严格相等

    对于原始值,使用严格相等进行比较

    对于引用值,递归进行深度比较

  • promisify(fn)

    将回调形式的函数转换为Promise的形式

    该方法会返回一个高阶函数,高阶函数就是采用Promise的形式

文件IO

fs模块

  • readFile

    读取文件内容

    const fs = require("fs");
    const path = require("path");
    
    const filePath = path.resolve(__dirname, "./test.txt");
    
    // 读取文件内容,文件内容默认会以Buffer的形式传递到回调的content参数中
    fs.readFile(filePath, (err, buffer)=>{
        console.log(buffer);
    }});
    
    // 读取文件内容,并将文件内容转为utf-8编码对应的字符,最后将字符传递到回调的content参数中
    fs.readFile(filePath, "utf-8", (err, content)=>{
        console.log(content);
    });
    
    // 同第二种方式
    fs.readFile(filePath, { encoding: "utf-8" }, (err, content)=>{
        console.log(content);
    });
    
  • writeFile

    向文件中写内容

    如果写入的文件不存在,则writeFile会自动创建该文件

    如果写入路径中的目录不存在,则直接报错

    const fs = require("fs");
    const path = require("path");
    
    const filePath = path.resolve(__dirname, "./test.txt");
    
    // 向文件写入字符串
    fs.writeFile(filePath, "content", (err)=>{
        console.log("覆盖成功");
    });
    
    // 编码默认就是utf-8,因此写入到文件中的默认就是字符串
    fs.writeFile(filePath, "content", "utf-8", (err)=>{
        console.log("覆盖成功");
    });
    
    // 内容除了可以是字符串,也可以是buffer
    fs.writeFile(filePath, Buffer.from("abcde", "utf-8"), (err)=>{
        console.log("覆盖成功");
    });
    
    // 默认重新写入的内容会覆盖掉原始的文件内容,可以设置flag为"a"来在原始文件内容后面追加新内容
    fs.writeFile(filePath, "content", {
        flag: "a",
        encoding: "utf-8"
    }, (err)=>{
        console.log("追加成功");
    });
    
  • stat

    获取文件或目录的状态信息

    const fs = require("fs");
    const path = require("path");
    
    const filePath = path.resolve(__dirname, "./test.txt");
    
    fs.stat(filePath, (err, stat)=>{
        console.log(stat);
        console.log(stat.size);						// 文件的大小(字节),目录则固定为0
        console.log(stat.atime);					// 最近访问该文件或目录的时间(时间戳)
        console.log(stat.mtime);					// 最近文件内容被修改的时间(时间戳)
        console.log(stat.ctime);					// 最近文件属性被修改的时间(时间戳)
        console.log(stat.birthtime);				// 文件或目录创建的时间(时间戳)
        console.log(stat.isDirectory());			// 是否是目录
        console.log(stat.isFile());					// 是否是文件
    });
    

    如果文件或目录存在,则stat为一个对象,err为null

    如果文件或目录不存在,则state为undefined,err为错误对象

  • readdir

    读取目录,得到该目录下的所有子文件和子目录的字符串

    const fs = require("fs");
    const path = require("path");
    
    const dirPath = path.resolve(__dirname, "./test");
    
    fs.readdir(dirPath, (err, files)=>{
        console.log(files);
    });
    
  • mkdir

    创建空目录

    const fs = require("fs");
    const path = require("path");
    
    const dirPath = path.resolve(__dirname, "./test");
    
    fs.mkdir(dirPath, (err)=>{
        console.log("创建目录成功");
    });
    
  • unlink

    删除文件

    const fs = require("fs");
    const path = require("path");
    
    const dirPath = path.resolve(__dirname, "./test.txt");
    
    fs.unlink(dirPath, (err)=>{
        console.log("文件删除成功");
    });
    
细节
  • 传递给fs模块中的函数的相对路径都是相对于CWD的,因此推荐传入绝对路径,这能够避免因运行node命令的运行位置不同造成结果上的差异

  • 在Node出现之前,ES6还没有出来,因此Node才会选择使用回调模式处理fs模块中的一部函数,随着ES6的发布,fs中也提供了支持promise的版本

    fs模块导出的对象中包含一个promises子对象,该对象中拥有和fs对象中同名且一样功能的函数,只不过promises中的函数都采用promise的形式进行使用

    const fs = require("fs");
    
    fs.promises.readFile(filePath, "utf-8").then((content)=>{
    	console.log(content);
    }).catch((err)=>{
        console.log(err);
    });
    
  • fs模块中绝大部分函数中的回调都是异步执行的,但对于这些函数,Node也提供了同步执行的版本,例如:readFile的同步版本是readFileSync

    Sync函数是同步执行的,会导致JS运行阻塞,影响程序执行的效率,因此Sync函数通常只会在程序启动的一开始出现,用于进行一些初始化操作

    const fs = require("fs");
    
    // readFileSync函数会将读取到的文件内容作为函数的返回值返回
    const content = fs.readFileSync(filePath, "utf-8");
    
    console.log(content);
    

流是指数据的流动,即数据从一个地方缓慢地流动到另一个地方

流是有方向的,根据方向的不同,可将Node中的流分为三种:

  1. 可读流(Readable Stream)

    数据从外部流向内存

  2. 可写流(Writable Stream)

    数据从内存流向外部

  3. 双工流(Duplex Stream)

    即可写也可读

之所以需要流,是因为外存于内存之间所能够存储的数据规模不一致,以及两者的数据处理速度不一致,这会导致某一方存储介质需要提前管理对方即将要处理的数据,导致自身存储空间的大量占用

下面中有更加清晰的解释

有了流后,就可以一部分一部分地让数据从一个地方流动到另一个地方,使得两者能够相互配合着工作

文件流

文件流即内存与磁盘文件之间的数据流动

原始的文件读取和写入操作(即readFile和writeFile)需要先将文件内容全部一次性读取到内存中,然后将内存中大量的数据一次性再写入到另一个文件中,这会导致内存资源的大量占用

有了文件流,就可以实现读一部分写一部分,避免了内存资源的大量占用

虽然流是一部分一部分传送数据的,但对比整体的数据传送时间,反而是流的方式更快,这在大文件传输中尤为明显

可读流

可读流可以让数据从文件流向内存,用于读取文件内容

创建文件可读流:

const fs = require("fs");
const path = require("path");

const filePath = path.resolve(__dirname, "./test.txt");

const rs = fs.createReadStream(filePath, {
    encoding: "utf-8",
    start: 0,
    end: Infinity,
    highWaterMark: 64 * 1024,
    autoClose: true
});
  • encoding

    指定读取出来的内容的形式

    默认为null,此时读取出来的数据将会成为buffer

    为"utf-8"时,读取出来的数据将会成为字符串

  • start

    从文件内容的起始字节位置开始读取

  • end

    读取到文件内容的结束字节位置为止

  • highWaterMark

    每次最多读取多少数据量的内容,默认值为64 * 1024

    具体读取内容的多少会受encoding的影响,例如:将encoding配置为"utf-8"时,此时highWaterMark为1表示每次读取一个字符,如果encoding为null,则highWaterMark为1表示每次读取一个字节

  • autoClose

    是否在读取完毕后自动关闭文件,默认为true

createReadStream会返回一个rs对象,该对象中包含了以下属性和方法:

  • on("事件名", 处理函数)

    open:文件打开事件,文件被打开时触发

    error:读文件时出错事件,例如读取的是不存在的文件时,会触发该事件

    close:文件关闭事件,文件被关闭时触发

    data:每读取完highWaterMark单位的数据后就触发一次该事件,需要注意的是,只有注册了该事件后,Node才会真正地开始读取数据,每次读取到的数据会传递给事件处理函数

    const fs = require("fs");
    const path = require("path");
    
    const filePath = path.resolve(__dirname, "./test.txt");
    
    const rs = fs.createReadStream(filePath, {
        encoding: "utf-8",
        start: 0,
        end: Infinity,
        highWaterMark: 64 * 1024,
        autoClose: true
    });
    
    rs.on("data", (chunk)=>{
        console.log(chunk);
    });
    

    end:读取数据完毕后触发

  • pause()

    暂停读取

    调用该方法后会触发pause事件

  • resume()

    恢复读取

    调用该方法后会触发resume事件

  • pipe(ws)

    可以将可读流与可写流进行连接,该方法可以缓解背压问题

    返回参数ws

    该方法实际上是对可读流与可写流之间互操作的封装:

    const fs = require("fs");
    const path = require("path");
    
    const from = path.resolve(__dirname, "./test.txt");
    const to = path.resolve(__dirname, "./test.copy.txt");
    
    const rs = fs.createReadStream(from);
    const ws = fs.createWriteStream(to);
    
    rs.pipe(ws);
    

    上面的代码等价于

    const fs = require("fs");
    const path = require("path");
    
    const from = path.resolve(__dirname, "./test.txt");
    const to = path.resolve(__dirname, "./test.copy.txt");
    
    const rs = fs.createReadStream(from);
    const ws = fs.createWriteStream(to);
    
    rs.on("data", (chunk)=>{
        const flag = ws.write(chunk);
        if(!flag){
    		rs.pause();				// 写入流管道已满,暂停可读流读取文件数据
        }
    });
    
    ws.on("drain", ()=>{			// 写入流管道清空时,继续让可读流读取文件数据
        rs.resume();
    });
    
    rs.on("close", ()=>{			// 可读流关闭后,关闭另一个文件
    	ws.end();
    });
    
可写流

创建文件可写流:

const fs = require("fs");
const path = require("path");

const filePath = path.resolve(__dirname, "./test.txt");

const ws = fs.createWriteStream(filePath, {
    flags: "a",
    encoding: "utf-8",
    start: 0,
    highWaterMark: 
});
  • flags

    默认为"w",表示新内容会覆盖文件的原有内容

    设置为"a"即可在原来的内容最后追加新内容

  • encoding

    写入的内容的形式

    默认为"utf-8",表示要写入的是字符串

    为null,表示要写入的是buffer

  • start

    从文件的起始字节位置开始写入

  • highWaterMark

    每次最多写入多少字节的内容,不受encoding的影响

  • autoClose

    是否在调用end方法时自动关闭文件,默认为true

createWriteStream会返回一个ws对象,该对象中包含了以下属性和方法:

  • on("事件名", 处理函数)

    open:文件打开事件,文件被打开时触发

    error:读文件时出错事件,例如读取的是不存在的文件时,会触发该事件

    close:文件关闭事件,文件被关闭时触发

    drain:当写入流通道从满状态重新恢复到空状态时,会触发该事件,利用该事件可以缓解背压问题

  • write(data)

    用于写入一组数据

    data可以是字符串或buffer

    返回一个Boolean值,返回的Boolean值代表了在写入完当前数据后,此时的写入流管道是否还未满

    写入流管道的大小取决于highWaterMark配置的数值,highWaterMark为10就表示写入流管道大小为10字节,也就能够一次性装下10字节的数据

    返回true时,表示写入流管道还没被填满,此时可以继续往管道中增加数据

    image.png

    返回false时,表示写入流管道已经被填满,此时如果继续写入数据,数据将会堆积在写入队列中

    image.png

    当写入队列中堆积着数据时,便会导致背压问题,背压问题会导致内存资源被大量占用

  • end([data])

    结束写入,并根据autoClose配置判断是否要自动关闭文件

    data参数是可选的,表示关闭前的最后一次写入的数据

net模块

利用net模块,可以实现一台主机中不同Node进程之间的通信,以及不同主机之间基于TCP连接进行的网络通信

创建TCP客户端

使用createConnection方法创建一个TCP客户端

创建TCP客户端时,需要指定要连接的TCP服务器的主机以及端口号,同时还需要传递一个回调函数,该函数会在TCP连接建立成功后被调用

createConnection方法会返回一个socket对象,该对象本质上是一个双工流

socket对象所操作(读写)的是一个socket文件,当socket对象往socket文件中写入内容时,写入的内容就可以通过已建立好的TCP连接传送给TCP服务器

TCP服务器发送过来的内容,也会通过TCP连接传送到socket文件中,之后socket对象就可以读取socket文件中的内容来得到TCP服务器传递过来的数据

image.png

const net = require('net');

const socket = net.createConnection({
	port: 9527,
    host: "localhost"
}, () => {
  console.log("TCP连接建立成功");
});

// 获取TCP服务器传递给自己的数据
socket.on("data", (chunk)=>{
    console.log(chunk.toString());
});

// 向TCP服务器发送数据
socket.write("...");

// TCP客户端主动关闭TCP连接
socket.end();

// 监听TCP连接的关闭
socket.on("close", ()=>{
    console.log("TCP连接已关闭");
});

创建TCP服务器

使用createServer方法创建一个TCP服务器

该方法会返回一个server对象,该对象中包含了以下属性:

  • listen(port, host)

    监听主机中的某个端口

  • on("事件名", 处理函数)

    listening:当TCP服务器监听某个端口成功后会触发该事件

    connection:当有TCP客户端与本TCP服务器建立TCP连接成功后会触发该事件,触发该事件时会为TCP客户端专门创建一个socket对象并传递给回调函数,该socket对象与创建客户端时返回的socket对象功能一致,利用该socket对象即可与TCP客户端进行数据交互

    不同的TCP客户端与同一TCP服务器建立的TCP连接是相互独立的,生成的socket对象也互不相同(操作的socket文件也不同),因此不同TCP客户端与同一个TCP服务器进行通信是互不干扰的

    image.png

const net = require('net');

const server = net.createServer();

server.listen(9527, "localhost");

server.on("listening", () => {
	console.log("监听端口9527成功");
});

server.on("connection", (socket)=>{
    console.log("与TCP客户建立TCP连接成功");
    
    // 读取TCP客户端发来的数据
    socket.on("data", (chunk)=>{
        console.log(chunk.toString());
    });
    
    // 向TCP客户端发送数据
    socket.write("...");
});

注意:利用net模块创建出来的是TCP客户端与TCP服务器,而不是http客户端和http服务器,它们不需要按照http协议规定的方式进行通信,即允许TCP双方在建立完TCP连接后,且在连接没有断开的期间通过连接通道随时随地地向对方发送任何格式的消息(因此通过net模块创建的TCP客户端与服务器,并没有什么请求响应的概念)

http模块

http模块建立在net模块之上(因为http协议是建立在TCP协议之上的),但http模块相较于net模块,不需要再手动管理socket的连接状态,此外,只需要开发者调用一些方法并传入相应数据,http模块在内部就会自动组装消息格式

发送请求

使用request来向服务器发送请求,request的第一个参数是请求的url,第二个参数是请求的一些配置,如请求方法、请求头等,第三个参数是回调函数,当服务器响应内容给本客户端时,会运行该回调函数,同时还会传递一个response对象到回调函数中

const http = require("http");

// 创建请求对象request,request是ClientRequest对象,本质上就是一个可写流
const request = http.request("http://localhost:9527", {		// 
    method: "GET",
    headers: {
        "Content-Type": "..."
    }
}, (response)=>{
    // response是IncomingMessage对象,本质上就是一个可读流
    console.log(response.statusCode);		// 获取响应的状态码
    console.log(response.headers);			// 获取响应头(对象)
    
    response.on("data", (chunk)=>{			// 由于响应体的数据量可能会很大,因此需要采用流的方式进行读取
        console.log(chunk.toString("utf-8"));
    });
    
    response.on("end", ()=>{
        console.log("响应体读取结束");
    });
});

// 书写请求体内容(可选)
request.write("...");

// 发送请求消息给服务器(只有请求消息发出去后,才有可能收到服务器的响应)
request.end();

创建http服务器

使用createServer方法创建一个服务器

createServer方法中可以传入一个回调函数,该回调函数会在有客户端向本服务器发出请求时被调用,并且该回调函数能够接收两个参数,分别是request对象和response对象

createServer方法会返回一个server对象,该server对象和net创建的服务器所返回的server对象用法相同

const http = require("http");

const server = http.createServer((request, response)=>{
    // request是IncomingMessage对象,response是ServerResponse对象
    console.log(request.headers);			// 获取请求头
    // request本质上就是一个可读流
    request.on("data", (chunk)=>{			// 获取请求体
        console.log(chunk.toString("utf-8"));
    });
    request.on("end", ()=>{
        console.log("请求体读取结束");
    });
    
    // response本质上就是一个可写流
    response.statusCode = 404;				// 设置响应码
    response.setHeader("a", "1");			// 设置响应头
    response.write("...");					// 设置响应体
    response.end();							// 发送响应消息给客户端
});

server.listen(9527, "localhost");

server.on("listening", ()=>{
    console.log("监听端口9527成功");
});

注意:使用http模块创建出来的客户端与服务器,是http客户端与http服务器,它们之间必须按照http协议的规定进行通信,且必须从请求开始,响应结束

node的事件循环

当执行node命令时,node会根据启动模块,将启动模块与其依赖的其他模块的所有同步代码执行完毕

之后,node会检查是否还有其他任务需要处理(是否有异步回调函数需要执行),如果有,则进入事件循环来处理这些任务;否则结束程序的运行

每一次的事件循环都会经过6个阶段,其中重点为以下三个阶段:

  1. timers

    该阶段用于处理计时器相关的回调

  2. poll

    该阶段中用于处理除timers和check的其他大部分回调

  3. check

    该阶段中用于处理setImmediate中的回调

剩下三个阶段不重要,本节课不做讲解

事件循环中的每个阶段都会维护一个独属于自己的回调执行队列,队列中用于存放该阶段所管理的可以执行了的回调函数

timers阶段维护的队列叫做计时队列,poll阶段维护的队列叫做轮询队列,check阶段维护的队列叫做检查队列

事件循环中的第一个阶段为timers阶段,在该阶段中,node会将计时队列中的所有到时回调依次拿出来执行,当把队列清空时便会进入下一阶段

下一个阶段是poll阶段,该阶段所管理的轮询队列所存放的回调函数包括:监听用户请求的回调、文件读取结束的回调等,在该阶段中,node会依次取出轮询队列中的待执行回调,将它们拿出来执行,直至把轮询队列清空

当poll队列为空(可能是被清空的,或者是本来就是空的),则node会检查其他5个阶段的队列是否为空:

  • 如果其他队列中有回调,则立即进入下一个阶段
  • 如果其它队列中都没有回调,则node会判断(6个阶段中)是否有回调还在等待着被加入到队列
    • 如果有,则node不会立即进入下一阶段,而是会在poll阶段中等待着其他阶段的队列中出现新回调,当这些队列中出现了新的回调函数,node就会直接进入下一阶段
    • 如果没有,快速经过本阶段以及之后的阶段,然后结束程序的执行

node在poll阶段等待的期间,如果轮询队列中出现了新的回调,则node会直接将其执行,执行完后还是会继续在该阶段等着,直至其他队列中出现新回调

check阶段的队列负责管理setImmediate中的回调,当代码中运行到setImmediate函数时,函数中的回调就会直接进入到此队列中,如果node来到该阶段并把该队列清空时,就会进入下一阶段

之后,node会循环上面的过程,直至将所有回调执行完,便会结束程序的运行

事件循环中除了这6大阶段所维护的队列外,还包含另外两个重要的异步队列,分别是nextTick队列和Promise队列,nextTick队列负责管理nextTick中的回调,Promise队列负责管理Promise中的异步回调

process.nextTick(()=>{
       console.log("nextTick");
});

事件循环的6大阶段中的回调,都是宏任务,而nextTick队列和Promise队列中存放的都是微任务,在任何一个宏任务被执行之前,都必须保证这两个微任务队列是空的

nextTick队列中任务的优先高于Promise队列中的任务,即Promise队列中的任务在执行时,需要保证nextTick队列是空的

image.png

特别补充

在Node中,计时器的延迟时间是无法做到0ms的(即使你设置的是0ms),最少也会是1ms,这就导致了下面这种情况:

setTimeout(()=>{
    console.log("setTimeout");
}, 0);

setImmediate(()=>{
    console.log("setImmediate");
});

输出结果:

"setTimeout"
"setImmediate"

"setImmediate"
"setTimeout"

上面的代码会出现两种不同的执行结果

假设计算机此时比较空闲,导致同步代码在1ms之内就被执行完,于是进入事件循环阶段,由于计时器还没有到时,因此计时器的回调就不会被加入到timers队列之中,之后来到check阶段时,就直接将setImmediate的回调拿出来执行了,这就导致先"setImmediate"再"setTimeout"

假设计算机正在忙于其他的一些工作,导致同步代码不能在1ms内执行完,于是1ms到时了,计时器的回调就会被加入到计时器队列之中,此时node一旦进入事件循环,便会先输出"setTimeout",再输出"setImmediate"

EventEmitter

EventEmitter是一个构造函数,使用该构造函数能够创建一个EventEmitter实例,利用EventEmitter实例可以自定义事件,当需要触发自定义的事件,需要通过该实例对象的emit方法进行

const { EventEmitter } = require("events");

const ee = new EventEmitter();

// 注册abc事件
ee.on("abc", ()=>{
    console.log("abc事件被触发了");
});

// 触发abc事件
ee.emit("abc");

自定义的事件,在触发时事件处理函数都是同步执行的

const { EventEmitter } = require("events");

const ee = new EventEmitter();

// 注册abc事件
ee.on("abc", ()=>{
    console.log("abc事件被触发了");
});

// 触发abc事件
ee.emit("abc");

console.log("script end");

/*
	"abc事件被触发了"
	"script end"
*/

在自定义的事件中可以为事件处理函数传递一些参数,此时在通过emit触发这些事件时,就需要在末尾加上提供对应的实参

const { EventEmitter } = require("events");

const ee = new EventEmitter();

ee.on("abc", (num, str)=>{
    console.log(num);
    console.log(str);
});

ee.emit("abc", 100, "hello");

除了可以使用on注册事件,还可以使用下面的方法注册事件:

  • addEventListener

    同on

  • once

    使用该方法注册的事件处理函数只会运行一次

EventEmitter实例中也提供了移除事件的方法,方法名叫做off,移除事件的方式和JS中的addEventListener的移除方式一样,需要通过函数的引用进行移除

const { EventEmitter } = require("events");

const ee = new EventEmitter();

const handler = ()=>{
    console.log("abc事件被触发了");
};

ee.on("abc", handler);

ee.emit("abc");

// 移除abc事件
ee.off("abc", handler);