关于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中各个模块进行了演绎总结,除了这些还有一个十分重要的模块:网络模块,将会在下一篇文章进行探讨,感谢您的浏览。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理