乐趣区

关于javascript:Nodejs剖析

一、Node 架构

1. Native modules

Native 层处于 Node 架构的顶层,由 JS 实现,次要提供应用程序可间接调用库,例如 fs、http 模块等。

2. Buildin modules(C/C++ Bindings)

该层充当了胶水层,因为 JavaScript 不能间接操作底层硬件设置,因而想要与底层进行通信就要用到该层与底层进行交互。

3. 底层(V8, libuv, c-ares(DNS), http parser, zlib(compression) … )

  • V8: 执行 JS 代码,提供桥梁接口
  • Libuv:事件循环、事件队列、异步 IO
  • 第三方模块

二、异步 IO

1. Node 的 IO 形式

IO 是应用程序的瓶颈所在,Node 单线程配合 事件驱动架构 及 libuv 实现了异步非阻塞的 IO,无需原地期待后果返回,性能较高,能够胜任 IO 密集型高并发申请。

2. 事件驱动架构

事件驱动架构相似于 公布订阅模式,即发布者播送音讯,订阅者订阅指定的音讯,并执行订阅到音讯后的代码。

const EventEmitter = require('events');
const myEvent = new EventEmitter();

// 订阅者
myEvent.on('event1', () => console.log(event1 executed));

// 发布者
myEvent.emit('event1');
// 订阅到后打印:// event1 executed

3. 单线程(主线程 V8)

Node 执行代码的线程也就是 V8 为单线程,单线程执行代码有时会呈现阻塞的状况。

创立一个函数,参数为 time,当 以后工夫 小于传入的工夫 + 函数执行时的工夫时,则进入循环中,直到条件不成立时进行函数返回。此时创立的 http 服务启动被阻塞。

const http = require('http');

function sleepTime(time) {const sleep = Date.now() + time * 1000;
  while (Date.now() < sleep) {}
  return 
}
// 调用提早函数
sleepTime(4); // 4s 后控制台打印 server is running
// 以下代码执行被阻塞
const server = http.createServer((req, res) => res.end('server is starting'));

server.listen(8000, () => console.log('server is running'));

三、全局对象

1. global

与浏览器的全局对象 window 有所不同,Node 应用 global 作为全局对象,是全局变量的宿主,换句话说全局变量都挂载在全局对象 global 身上。

常见的全局变量:

  • __filename: 返回正在执行脚本文件的绝对路径
  • __dirname: 返回正在执行脚本所在目录
  • timer类函数:执行程序与事件循环间的关系
  • process:提供与以后过程互动的接口
  • require:实现模块的加载
  • module/exports:解决模块的导出

2. process

process 能够查看无关过程的信息。

process.memoryUsage(): 返回一个对象,形容 Node.js 过程的内存应用状况(以字节为单位)。

process.cpuUsage(): process. cpuusage()办法在一个具备 user 和 system 属性的对象中返回以后过程的用户和零碎 CPU 工夫应用状况,它们的值是微秒值(百万分之一秒)。这些值别离度量在用户和零碎代码中破费的工夫,如果有多个 CPU 内核为该过程执行工作,则可能最终大于理论运行工夫。

之前调用 process.cpuUsage()的后果能够作为参数传递给该函数,以取得差别读取。

process.cwd():过程工作目录,也就是执行 node 命令时所在的目录。

process.version: node 版本

process.versions: node 环境版本

process.arch: cpu 架构

process.env.NODE_ENV:生产 / 开发环境

process.env.path: 零碎环境变量

process.env.USERPROFILE: 以后用户目录(windows, macos 为 home)

process.platform:运行平台

process.uptime:过程曾经运行的工夫(秒)

process.on()监听事件:

process.on('exit', (code) => {console.log('exit' + code);
})

process.on('beforeExit', (code) => {console.log('before exit' + code);
})

console.log('代码执行结束');

// 代码执行结束
// before exit 0
// exit 0

beforeExit传入的函数能够有异步代码,而 exit 只能有同步代码,否则异步代码将不会执行。

process.stdin:规范输出

process.stdout:规范输入

process.stdin.pipe(process.stdout); 
// 控制台阻塞,输出 111
// 控制台打印 111

process.stdin.setEncoding('utf-8');
// 启动输出监听,如果可读执行回调函数
process.stdin.on('readable', () => {let chunk = process.stdin.read();
  if (chunk !== null) {process.stdout.write('data', chunk);
  }
})

四、path 模块

1. 罕用 API

  • path.basename(path, exd): 获取门路中的根本门路(门路的最初一部分),第二个参数为扩展名,如何增加返回值不带扩展名,开端有宰割符则会疏忽(/a/b/c.js/ -> c.js)。
  • path.dirname(path):获取门路中的目录名称,开端的宰割符也会疏忽(/a/b/c/ -> /a/b)。
  • path.extname(path): 获取门路中扩大名称
  • path.isAbsolute(path):获取门路是否为绝对路径
  • path.join(path1, path2, ...):拼接多个门路片段
  • path.resolve(path1, path2, ...):返回绝对路径,如果非第一个参数呈现绝对路径,返回的绝对路径将从该门路开始。
  • path.parse(path): 解析门路,返回一个对象,例如传入一个相对目录门路 ’/a/b/c’,会返回{root: ‘/’, dir: ‘/a/b’}
  • path.format(pathObj):序列化门路, 承受一个 parse 办法解析后的门路对象,返回残缺门路。
  • path.normalize():规范化门路

五、Buffer

最后的 JavaScript 服务于浏览器平台,外部次要操作的数据是字符串,随着 Nodejs 平台的呈现使得 JavaScript 能够实现 IO 操作,因而就呈现了 Buffer 的存在,为什么 IO 操作会与 Buffer 扯上关系,咱们先从二进制数据开始说起。

1. 二进制数据

服务器在解决与用户交互的信息时,该信息就是二进制数据,所以 IO 操作的就是二进制数据。

2. Stream 流操作

Stream 流操作并非是 Nodejs 独创,它相当于一种数据类型,也可能用于存储数据,而且最次要的是可能进行分段,当传输数据较大时,便能够应用流操作,因而能够防止因为操作的数据过大而把内存短时间占满的状况,再配合管道技术即可实现数据分段传输。

3. Buffer

有了下面两点,二进制数据的传输是端到端的,能够了解为有一个数据的 生产者 (服务端),至多有一个 消费者(客户端)。此时会有多种状况产生,比方数据的生产速度可能无奈满足数据的生产速度,或者数据的生产速度比生产速度慢一些,那么数据就会呈现期待的状况,可能是前者未达到需要的数据在期待,也可能是后者多出的数据在期待,因而这些数据存储在哪就成了一个问题,这就有了 Buffer 的产生。

Nodejs 中的 Buffer 是一片内存空间,是无需 require 的一个全局变量,实现了 Nodejs 平台下的二进制数据操作,且不占据 V8 堆内存大小的内存空间,由 C ++ 层进行间接调配。,个别配合 Stream 流 应用,充当缓冲区。

4. 创立 Buffer

  • alloc:创立指定 字节 大小的 buffer
  • allocUnsafe:创立指定大小的 buffer(不平安)
  • from:承受数据,创立 buffer
// 创立 10 字节大小 buffer
const b1 = Buffer.alloc(10);
console.log(b1); // <Buffer 00 00 00 00 00 00 00 00 00 00> 16 进制

const b2 = Buffer.allocUnsafe(10);
console.log(b2); // <Buffer 08 00 00 00 01 00 00 00 00 00>
// 内存当中只有有闲暇空间就会被应用

// 默认第二个参数为 utf- 8 编码
const b3 = Buffer.from('1');
console.log(b3); // <Buffer 31>

// 接管数组
const b4 = Buffer.from([1, 2, 3]);
console.log(b4); // <Buffer 01 02 03>

5. Buffer 实例办法

  • fill:应用数据填充 buffer
  • write:向 buffer 中写入数据
  • toString:从 buffer 中提取数据
  • slice:截取 buffer
  • indexOf:在 buffer 中查找数据
  • copy:拷贝 buffer 中的数据
const buf = Buffer.alloc(6);

// fill 会将 buffer 填满
buf.fill('123');
console.log(buf.toString()); // 123123
// 后两个参数为填充起始地位与完结地位,且完结地位取不到

// write
const buf1 = Buffer.alloc(6);
buf1.write('123', 1, 4) // value offset length
console.log(buf1.toString()); // 123

// slice 与字符串的 slice 操作相似
const buf2 = Buffer.from('hello');
buf2.slice(-3);
console.log(buf2.toString()); // llo

// indexOf
const buf3 = Buffer.from('he, hello');
console.log(buf3.indexOf('h')) // 0
// 查找的起始地位
console.log(buf3.indexOf('h', 2)); // 6
// 返回的是字节,如果有中文存在,一个中文占 3 个字节

// copy
const b4 = Buffer.alloc(6);
const b5 = Buffer.from('hello.');
// 将 b5 的值赋值给 b4
b5.copy(b4, 3, 3, 6); // 赋值的 buf 写入地位 读取地位 读取完结地位(取不到)

6. 静态方法

  • concat([buf1, buf2, ..], totalLength):将多个 buffer 拼接成一个新的 buffer
  • isBuffer:判断以后数据是否为 buffer

7. 自定义办法

// 实现 buffer 的 split 办法
ArrayBuffer.prototype.split = function(sep) {
  // 获取分隔符的长度
  const len = Buffer.from(sep).length;
  const ret = [];
  const start = 0;
  const offset = 0;
  
  while (offset = this.indexOf(sep, start) !== -1) {ret.push(this.slice(start, offset));
    start = offset + len;
  }
  // 最初一个字符匹配上
  res.push(this.slice(start))
  return ret;
}

六、File System

fs是 Node 内置的外围模块,次要是进行文件的 读写或拷贝 操作。

学过操作系统都晓得文件系统中的文件领有可读、可写以及执行的权限,个别文件的默认权限为可读可写不可执行(r-w-),用 8 进制示意为 0666,转换为十进制为 438。

在 Node 中应用 flag 示意对文件的操作方法,比方可读可写。

常见的的 flag 操作符:

  • r: 可读
  • w: 可写
  • s:示意同步
  • +:执行相同操作
  • x:示意排他操作
  • a:示意追加操作

1. 罕用的文件操作 API

新建一个 data.txt 文件,内容为:

Hello World
// readFile
const fs = require('fs')
const path = require('path');

fs.readFile(path.resovle('data.txt'), 'utf-8', (err, data) => {console.log(data); // Hello World
  console.log(err); // null
});

// writeFile
// mode: 执行权限
// flag 默认为 w +,写入时会清空再写入。应用 r + 会间接笼罩
fs.writeFile('data.txt', 'abc', { mode: 438, flag: 'r+', encoding: 'utf-8'}, (err) => {if (!err) fs.readFile('data.txt', 'utf-8', (err, data) => {console.log(data); // abclo World
  })
});

// appendFile 在内容后追加内容
fs.qppendFile('data.txt', 'append part', (err) => {});

// copyFile
fs.copyFile('data.txt', 'test.txt', (err) => {});

// watchFile 监听文件 interval: 监听距离
fs.watchFile('data.txt', { interval: 20}, (current, previous) => {
  // 批改之后文件的批改工夫与之批改之前的文件批改工夫不同阐明文件已被批改
  if (current.mtime !== previous.mtime) {console.log('modified');
    // 勾销监听
    fs.unwatchFile('data.txt');
  }
})

2. 文件关上与敞开

下面应用 Nodejs 进行文件的读写曾经可能实现文件的关上,那么为什么还要提供文件关上的 api?因为 readFilewriteFile是一次性进行读取或写入,但对于较大体积的文件正当的解决形式是边读取边写入,所以须要将文件的关上、读取、敞开看成独立的环节。

// 关上敞开文件
// fd(file descriptor): 文件描述符
fs.open('data.txt', 'r', (err, fd) => {console.log(fd);
    fs.close(fd, (err) => {console.log('close');
    });
});

3. 大文件读写

对于大文件须要先关上文件再进行读取而后写入 buffer 中,再读取 buffer 中的数据写入到文件中。

const buf = Buffer.alloc(10);

fs.open('data.txt', 'r', (err, rfd) => {console.log(rfd);
    // 1:buffer 写入地位 4: 写入 lenth 0 读取地位
    fs.read(rfd, buf, 1, 4, 1, (err, readBytes, data) => {console.log(readBytes);
        console.log(data.toString());
    });
});    

const buf1 = Buffer.from('12345')

// write:读取 buffer 写入到文件中
fs.open('b.txt', 'w', (err, wfd) => {console.log(wfd);
    // 0: buffer 读取地位
    fs.write(wfd, buf1, 0, 3, 0, (err, written, data) => {console.log(written);
        console.log(data.toString());
    })
})

实现文件的拷贝:

import fs from 'fs';

const buf = Buffer.alloc(10);
let positon = 0;

fs.open('b.txt', 'r', (err, rfd) => {fs.open('a.txt', 'w', (err, wfd) => {function next() {fs.read(rfd, buf, 0, buf.length, positon, (err, bytesRead, buffer) => {if (!bytesRead) {fs.close(rfd);
          fs.close(wfd);
          console.log('拷贝实现');
          return;
        }
        positon += bytesRead;
        fs.write(wfd, buf, 0, bytesRead, (err, written) => {
          // 递归读写
          next();})
      })
    }
    next();});
});

4. 目录操作

  • fs.access():判断文件或目录是否具备操作权限
  • fs.stat(): 获取目录及文件信息
  • fs.mkdir():创立目录,如果想创立多个子目录如a/b/c,须要在第二个参数加上{recursive: true}
  • fs.rm(): 删除目录或文件,想要删除子目录下时也需加上{recursive: true}
  • fs.readdir(): 读取目录中内容
  • fs.unlink(): 删除指定文件

七、模块化

1. CommonJS

CommonJS是 Nodejs 的模块化标准,跟 esmodule 模块化计划相似。

次要特点为:

  • 任意一个文件就是一模块(module),具备独立作用域

    • 每个模块都会传入一个 module 对象,该对象有以下属性:

      • module.id:返回模块标识符,个别是一个绝对路径
      • module.filename: 返回文件模块的绝对路径
      • module.loaded: 返回布尔值,示意模块是否实现加载
      • module.children: 返回数组,寄存以后模块调用的其余模块
      • module.exports: 返回以后模块裸露的内容
      • paths:返回数组,寄存不同目录下的 node_modules 地位

    在 node 中导出以后模块的内容还有 exports,那么于module.exports 区别是什么?

    exports指向了 module.exports 的 内存地址,所以不能给 exports 从新赋值,不然会切断与 module.exports 的 分割。

  • 应用 require 导入其余模块(同步加载)

    • 基本功能是读入并且执行一个模块文件:依据传入的模块的后缀进行查找,如果没有后缀会依照 .js, .json, .node 进行填充查找,而后同步读取文件的内容,将读取的字符串转为函数,而后调用并传入 exportsfilename 等全局变量。
    • resolve:返回模块文件绝对路径
    • extensions: 根据不同后缀名执行解析操作
    • main:返回主模块办法
  • 将模块 ID 传入 require 实现目标模块定位

2. 实现 require 函数

const fs = require('fs');
const path = require('path');
const vm = require('vm');

// Module 类
function Module(id) {
    this.id = id;
    this.exports = {};}

// 解析传入文件名称参数
Module._resolveFilename = function (filename) {const absPath = path.resolve(__dirname, filename);
  // 如果存在间接返回
    if (fs.existsSync(absPath)) {return absPath;}
  // 不存在则增加后缀
    const suffix = Object.keys(Module._extensions);

    for (let i = 0; i < suffix.length; i++) {const newPath = path.resolve(__dirname, filename + suffix[i]);
        if (fs.existsSync(newPath)) {return newPath;}
    }
    throw new Error(`${filename} is not exists`);
};

// Module 扩展名
Module._extensions = {'.js'(module) {let content = fs.readFileSync(module.id, 'utf-8');

        content = Module.wrapper[0] + content + Module.wrapper[1];

        // vm
        const compileFn = vm.runInThisContext(content);

        const exports = module.exports;
        const __dirname = path.dirname(module.id);
        const __filename = module.id;

    // 模块 this 指向 exports
        compileFn.call(exports, exports, __dirname, __filename, myRequire, module);
    },
    '.json'(module) {let content = fs.readFileSync(module.id, 'utf-8');
    content = JSON.parse(content);
    module.exports = content;
    },
};

Module.wrapper = ['(function(exports, __dirname, __filename, require, module){',
    '})',
];

Module._cache = {};

Module.prototype.load = function () {const extname = path.extname(this.id);
    Module._extensions[extname](this);
};

function myRequire(filename) {
    // get absolute path
    const mPath = Module._resolveFilename(filename);

    // cache
    const cacheModule = Module._cache[mPath];

    if (cacheModule) return cacheModule.exports;

  // 不存在缓存先创立空实例
    let module = new Module(mPath);
    // 存入缓存
    Module._cache[mPath] = module;
    // 加载模块
    module.load();

    return module.exports;
}

const obj = myRequire('./vm');
console.log(obj);

八、Events 模块

1. 常见 api

  • on:增加当事件被触发时调用的回调函数
  • emit:触发事件,依照注册的序同步调用每个函数监听器
  • once:增加当事件在注册之后首次被触发时调用的回调函数(注册后只触发一次)
  • off:移除特定的监听器
const EventEmitter = require('events');

const ev = new EventEmitter();

ev.on('event1', () => console.log('event1')); 
ev.once('event2', () => console.log('event2'));
ev.emit('event1');
ev.emit('event1');
ev.emit('event2');
ev.emit('event2');
// event1 
// event1
// event2

const foo = () => console.log('event3');
ev.on('event3', foo);
ev.emit('event3');
ev.off('event3', foo);
ev.emit('event3');
// event3

2. 公布订阅模式

公布订阅模式里发布者公布音讯,订阅者订阅音讯这一行为并非是被动的,而是通过 调度核心 进行执行,有利于发布者和订阅者两者之间的解耦。

实现:

class PubSub {constructor() {this.events = {};
  }
    // 注册事件
  subscribe(event, callback) {if (this.events[event]) {this.events[event].push(callback);
    } else {this.events[event] = [callback];
    }
  }
// 公布事件
  publish(event, ...args) {const items = this.events[event];
    if (items?.length) {items.forEach(function(callback) {callback.call(this, ...args);
      });
    }
  }
}

let ps = new PubSub();
ps.subscribe('event1', () => {console.log(1);
})
ps.publish('event1');
ps.publish('event1');
ps.publish('event1');
// 1
// 1
// 1

九、事件循环

1. 浏览器中的事件循环

在浏览器中代码的执行程序是先执行同步代码,遇到异步工作则放入事件循环队列中,异步工作分为 宏工作 微工作 ,像PromiseMutation Observer 以及 Node 中的 Process.nextTick 为微工作,其余则为宏工作。每执行一个宏工作都会顺带清空微工作,所以能够事件循环看作排队,宏工作是次要工作,微工作则是顺带着做的一些事,所以每执行一个宏工作都会立即查看微工作队列。

setTimeout(() => {console.log('s1'); // 2
  Promise.resolve().then(() => {console.log('p2'); // 3
  })
  Promise.resolve().then(() => {console.log('p3'); // 4
  })
})

Promise.resovle().then(() => {console.log('p1'); // 1
  setTimeout(() => {console.log('s2');  // 5
  })
  setTimeout(() => {console.log('s3'); // 6
  })
})

2. Node 中的事件循环

与浏览器中的宏工作、微工作循环队列不同,Node 中有 6 个事件循环队列:

  • timers: 执行 setTimeoutsetInterval回调
  • pending callbacks:执行零碎操作的回调,例如 tcp、udp
  • idle,prepare:只在零碎外部进行应用
  • poll:执行与 I / O 相干的回调
  • check:执行 setImmediate 中的回调
  • close callbacks:执行 close 中的回调

Node 中代码的执行程序为:

  • 执行同步代码,将不同的工作增加至相应的队列中
  • 所有同步的代码执行后开始执行满足条件的微工作
  • 所有微工作代码执行后会执行 timer 队列中满足条件的宏工作
  • timer 队列执行实现后顺次切换队列(在实现队列切换前会先清空微工作代码)
setTimeout(() => { // timer 5
  console.log('s1');
})

Promise.resolve().then(() => { // 微工作 4
  console.log('p1');
})

console.log('start'); // 1

process.nextTick(() => { // 微工作 3 优先级高于 Promise
  console.log('tick');
})

setImmediate(() => { // check 6
  console.log('setimmediate');
})

console.log('end'); // 2

浏览器和 Node 的次要区别是:浏览器中每执行完一个宏工作就会清空微工作队列,而 Node 中只有队列工作全副实现在切入下一个队列之前才会清空微工作队列

3. 常见问题

setTimeout(() = {console.log('timeout');
}, 0);
setImmediate(() => {console.log('immediate');
})

下面这段代码多运行几次会发现输入程序会发生变化,有时先输入 timeout,有时先输入 immediate,这是因为 setTimeout 的延时工夫为 0 时会呈现延时不固定的起因。

放入回调函数会解决该问题:

const fs = require('fs');
fs.readFile('./test.txt', () => {setTimeout(() = {console.log('timeout');
}, 0);
  setImmediate(() => {console.log('immediate');
  })
})
// immediate
// timeout

此时执行程序固定,这是因为当 readFile 函数的回调是在 poll 队列中,执行完该回调后会向 timer 中塞入 setTimeout,check中塞入 setImmediate,而后向下执行 check 队列的工作最初回到 timer 中执行其中的工作。

这样就保障了执行程序的固定。

十、Stream 流模块

1. Node 中的流

Nodejs 中流就是解决流式数据的形象接口。

Node 中流的分类:

  • Readable:可读流,可能实现数据的读取
  • Writeable:可写流,实现数据的写入
  • Duplex:双工流,即可读又可写
  • Transform:转换流,可读可写,并能实现数据转换

Node 中流的特点:

  • Stream模块实现了四个具体的形象(类)
  • 所有的流继承自EventEmitter

2. 可读流

可读流的作用就是为了生产数据,处于构造的上游,要想实现本人的可读流须要继承 Node 中 Readable 类并重写 _read 办法。

const {Readable} = require('stream');

// 继承 Readable
class MyReadable extends Readable {constructor(source) {super();
        this.source = source;
    }
    _read() {
    // 最初传入 null 以判断是否读完
        const data = this.source.shift() || null;
        this.push(data);
    }
}

const myReadable = new MyReadable(['I', 'am', 'name6']);

myReadable.on('readable', () => {
  let data = null;
  // 2 示意每次读取大小
  while ((data = myReadable.read(2)) !== null) {console.log(data.toString()); // Ia mn am e6
  }
})

myReadable.on('data', (chunk) => {console.log(chunk.toString()); // I am name6
})

3. 可写流

可写流是用于生产数据的流,处于构造的上游。实现本人的可写流要继承 Writeable 类并重写 _write 办法。

const {Writeable} = require('stream');

class MyWriteable {constructor() {super();
  }
  _write(chunk, en, done) {process.stdout.write(chunk.toString());
    process.nextTick(done)
  }
}

const myWriteable = new MyWriteable();
myWriteable.write('...', 'utf-8', () => {console.log('end');
})

// ...
// end

4. 双工流与转换流

双工流为可读流与可写流的联合,继承自 Duplex 类后重写 _read 与_write 办法,可读可写的操作与可读流、可写流操作一样。

转换流比双工流多了数据转换的性能:

const {Transform} = require('stream');

class MyTransform {constructor() {super();
  }
  
  _transform(chunk, en, cb) {
    // 进行数据转换
    this.push(chunk.toString().toUpperCase());
    cb(null);
  }
}

const myTransform = new MyTransform();
t.write('a');
t.on('data', (chunk) => {console.log(chunk.toString()); // A
})

总结

以上对 Node 中各个模块进行了演绎总结,除了这些还有一个十分重要的模块:网络模块,将会在下一篇文章进行探讨,感谢您的浏览。

退出移动版