liucong5 2c23469e78 feat: 添加CJS的一些说明 2 năm trước cách đây
..
Image 2c23469e78 feat: 添加CJS的一些说明 2 năm trước cách đây
TestJS 2c23469e78 feat: 添加CJS的一些说明 2 năm trước cách đây
README.md 2c23469e78 feat: 添加CJS的一些说明 2 năm trước cách đây

README.md

NodeJS 的学习

NodeJs介绍

什么是NodeJS?

官方定义是:NodeJs Is a JavaScript Runtime Built On Chrome's v8 JavaScript Engine
NodeJs 是一个基于 V8 Javascript 引擎的 Javascript 运行时环境

上图是一个渲染引擎的工作流程

  1. 首先将 HTML 通过 HTMLParser 解析生成 Dom 树
  2. 将 StyleSheets 通过 CSSParser 解析成 StyleRules
  3. 将 StyleRules 附加到 Dom 树上,生成 RenderTree,也就是渲染树
  4. RenderTree 和 Layout(布局) 结合之后就可以进行绘制

前端渲染引擎是浏览器中负责将 HTML、CSS 和 JavaScript 转换为可视化页面的核心组件

Javascript 可以操作上面的 Dom 树,所以为了执行 Javascript 代码的逻辑,需要一个解释器来解释 JavaScript 代码并将其翻译成机器语言执行

NodeJS 中运行 JavaScript 代码是通过 V8 引擎的

  • V8 是用 C++ 编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎
  • 它实现 ECMAScript 和 WebAssembly,并且实现Windows、MacOS和Linux的跨平台
  • V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中

V8 的运行流程

NodeJS 不仅仅只有 V8 引擎,V8 只是帮助 JS 代码执行,除此之外 NodeJS 还需要处理文件系统读/写、网络IO、加密、压缩解压缩等操作

NodeJS 和 浏览器 的差异

NodeJS 的架构图

  • 上图左上角的 APPLICATION 表示用户所写的 Javascript 代码
  • 用户写的 JS 代码交给 V8 来执行
  • V8 执行的代码的时候会通过 NODE.JS BINDINGS 也就是 NodeJS 的 API 来连接 LIBUV
  • LibUV 中包括事件循环、文件系统、网络等核心模块

LibUV 是用 C语言 编写的库

安装 NodeJS

安装完毕之后,可以测试使用 NodeJS 来运行 JS 代码

直接在命令行中输入 node + 要运行的JS文件即可

Node的REPL

什么是REPL?

REPL 是 Read-Event-Print Loop 的简称,即 读取-求值-输出的循环 REPL 是一个简单的、交互式的编程环境

比如直接在命令行输入 python,就是进入 python 的 REPL 交互环境

node 与 python 类似,在命令行中直接输入 node,就是进入 Node 的 REPL 的交互环境

process 翻译过来就是进程,在 node 中是一个全局变量,存储了很多有用信息

Node 程序传递参数

按照之前所讲,Node 运行 JS 文件只需要通过 node 文件名.js 即可执行对应文件

那么如果想要给执行的js文件传递参数又该如何处理?

console.log(process) 在 node 执行的 js 文件中输出 process 的内容

然后使用 node 文件名.js t1 t2 t3 去执行指定的 js 文件

这里 t1、t2、t3 就是模拟参数输入

在 process 中可以找到 argv 属性,它的值是一个数组

{
    argv: [
        'E:\\nodejs\\node.exe',
        'F:\\KS-TS\\MarkdownLog\\NodeJS\\TestJS\\Test_02.js',
        't1',
        't2',
        't3'
    ],
    execArgv: []
}
  • 'E:\\nodejs\\node.exe' 表示 node 可执行程序所在路径
  • 'F:\\KS-TS\\MarkdownLog\\NodeJS\\TestJS\\Test_02.js' 表示执行的js文件所在路径
  • 剩下的就是命令行传入的参数了

那么为了输出所有的参数,使用数组的遍历即可

process.argv.forEach((val, index) => {
    console.log(`index = ${index} value = ${val}`)
})

  • argc:argument counter的缩写,传递参数的个数
  • argv:argument vector的缩写,传入的具体参数

vector翻译过来是矢量的意思,在程序中表示的是一种数据结构

Node 的输出

console.log 最常用的输入内容的方式

console.clear 清除输出

console.trace 跟踪,输出调用栈

console的一些调用API

遇到问题直接查找 Node 的 API 文档,更加准全

Node 常见的全局对象

全局对象可以在程序的任何位置都可以访问到

官方文档中说明的Global对象

并不是所有的全局对象都会被用到

模块化相关的 exportsmodulerequire() 经常用到

web服务器会用到 URL

有一些特殊的全局对象是每个模块都私有一份的,而不是整个程序通用的,比如:__dirname__filenameexportsmodulerequire()

  • __dirname:目录名称
  • __filename:文件名称

但是 __dirname 和 __filename 等特殊全局对象在 REPL 环境下是无效值

还有一些比较常用的全局对象

  • process:提供了 node 的进程相关的信息
    • 比如 node 的运行环境、参数信息等
    • 还可以一些环境变量读取到 process 的 env 中
  • console:提供了简单的调试控制台
  • 定时器函数: setTimeoutsetIntervalsetImmediate
    • setImmediate:它的作用是将回调函数放入事件循环的检查阶段,以便在当前一轮事件循环的末尾立即执行
    • 除此之外,process.nextTick 也可以在下一帧执行某个操作
  • global对象

在 REPL 环境中,输入 gloabl. 然后双击 tab 即可得到 global 所有的属性

通过上面 global 的属性图可以看到,global中封装了很多常用属性,包括 DateArraySetObjectURLv8 等常用数据对象

为什么要将很多数据对象放在 global 中?因为方便获取,参考浏览器中的 window 对象,方便使用者调用

global.process 就是全局对象 process global.process === process

参考 node 的源码,就是直接将 process 设置为 global 的属性

global === globalThis,两个完全一样

JavaScript 的模块化

基于node进行开发时,绝大多数情况都是编写 JS 代码

什么是模块化?

  • 事实上模块化开发的最终目的是将程序分成一个个小的结构
  • 不同结构编写属于自己的逻辑代码,并且有自己的作用域,不会影响到其他结构
  • 每个结构可以将自己希望暴露的变量函数对象等导出给其他结构使用
  • 每个结构可以通过某种方式,导入其他结构的变量函数对象

上面的结构就是模块;按照这种结构划分的过程,就是模块化开发的过程

node 中使用的模块规范是 CommonJS

CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,简称为CJS

  • NodeCommonJS在服务器端一个具有代表性的实现
  • BrowserifyCommonJS在浏览器中的一种实现
  • webpack打包工具具备对CommonJS的支持和转换

Node中对 CommonJS 进行了支持和实现,帮助可以方便的进行模块化开发

  • Node中每一个 JS 文件都是一个单独的模块
  • 单独的模块中包括 CommonJS 规范的核心变量:exportsmodule.exportsrequire,可以使用这些变量进行模块化开发

exportsmodule.exports 负责导出,但是两者是不一样的

require函数可以帮助导入其他模块(自定义模块、系统模块、第三方模块)

测试案例1

为了证明一个JS文件就是一个模块

新建两个js文件:bar.jsmain.js

bar.js 中定义属性和函数,在main.js中直接调用bar.js中定义的属性和函数

// bar.js
const name = "bar.js"

const age = 10

let message = "my name is bar.js"

function barFunc(name) {
    console.log("hello " + name);
}
// main.js
console.log(name)
console.log(age)

最后的结果就是报错,在main.js中并不能找到name属性

测试案例2

将前面 bar.js 中定义的属性和函数导出

还记得前面提到的全局对象吗?其中有一个叫 exports 的特殊全局对象,它是每个模块都有一个的对象

exports 是一个对象,那么就可以给对象添加属性,属性就会跟着 exports 对象一起被导出

// bar.js
const name = "bar.js"

const age = 10

const obj = {
    name: "bar",
    age: 10
}

let message = "my name is bar.js"

function barFunc(name) {
    console.log("hello " + name);
}

exports.name = name
exports.age = age
exports.obj = obj

setInterval(() => {
    console.log(obj.name)
    console.log(age)
}, 1000);

main.js需要导入对应的bar.jsnameage

// 获得整个对象
const bar = require('./bar')

console.log(bar.name)
console.log(bar.age)

// 通过解构获得对象对应的属性
const {name, age} = require('./bar')

console.log(name)
console.log(age)

setTimeout(() => {
    bar.obj.name = "main"
    bar.age = 0;
}, 1500);

通过require() 会返回一个对象,这个对象就是 bar.jsexports 对象

参考 bar.jsinterval 回调函数的输出,可以发现 obj 的属性被修改了(即使 obj 是 const 的),因为 obj 是浅拷贝

但是 bar.js 中的 age 作为 number,是值传递,所以 main.js 不能对 bar.js 中的 age 产生修改

通过输出结果的变化,可以论证 require('./bar') 函数的返回值就是 bar.js 中的 exports 对象

每个模块的 exports 对象默认是一个空对象 exports = {}

所以单从 requireexports 来看,就是一个浅拷贝罢了

测试案例3

module.exports 是什么?

CommonJS中是没有 module.exports 的概念的,但是为了实现模块的导出,node中使用的是 Module 的类,每一个模块都是 Module 的一个实例,也就是 module

let module = new Module()

所以在 Node 中真正用于导出的其实根本不是 exports,而是 module.exports,因为 module 才是导出的真正实现者

console.log(module.exports === exports)// true
console.log(module) // 查看对象的所有属性

module 对象中有个 exports 属性,exports 属性存储了所有设置的导出对象

node 的逻辑大概是将 exports 赋值给 module 的对应属性中

module.exports = exports

本质上是 module.exprots 在导出,为了验证这个观点,我们对 module.exports 做一些操作

// bar.js
const name = "bar.js"

const age = 10

exports.name = name
exports.age = age

module.exports = {}
// main.js
const bar = require('./bar')

console.log(bar.name)   // undefined
console.log(bar.age)    // undefined

命名 bar.jsexports 对象中存在 nameage 属性,但是 main.js 获得的对象却没有 nameage 属性

由此可见,导出本质上是导出 module.exports 而不是 exports 对象

那么 exports 对象有什么存在的必要呢?

因为 CommonJS 的规范要求必须有一个 exports 对象作为导出,nodejs 为了满足 CommonJS 做出了一种妥协

测试案例4

// bar.js
exports = 123

// main.js
console.log(require(`./bar`)) // 输出 {}

根据 main.js 的输出可以得出结论, module.exports = exports 赋值是在文件一开始就做了,如果赋值是在文件最后做的话 main.js 应该输出 123 才对

关于 require 的细节

官方文档中 require 的查找细节

require 是一个函数,可以帮助引入一个文件(模块)中导入的对象

一些比较常用的规则(规则很多不全部介绍),导入格式:require(X)

  1. 如果 X 是一个核心模块,比如pathhttpfs
    • 直接返回核心模块,并停止查找(优先查找核心模块)
  2. 如果 X 是以 ./ 或者 ..//(根目录) 开头的

    • 说明是查找文件或者文件夹
    • 如果是查找文件
      • 如果有后缀名,按照后缀名的格式查找对应的文件
      • 如果没有后缀名,按照如下顺序查找
      • 直接查找文件 X
      • 查找 X.js 文件
      • 查找 X.json 文件
      • 查找 X.node 文件
    • 如果没有找到对应文件,那么将 X 看成是目录
      • 查找目录下的 index 文件
      • 查找 X/index.js 文件
      • 查找 X/index.json 文件
      • 查找 X/index.node 文件
    • 如果都没有找到,那就报错:not found
  3. 如果直接是一个 X ,不是路径也不是一个核心模块

    • 优先查找是否是核心模块
    • 然后在运行 require 函数的js文件同级目录的 node_modules 中查找
    • 然后在上一层级的 node_modules 文件夹中查找
    • 更上一级的 node_modules 文件夹中查找
    • 直至查找到根目录位置

假设当前运行 require 函数的是 main.js 文件,其路径是 /User/codewhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js 那么其 require 的查找路径如下

console.log(module.path)
console.log(module.paths)

通过 module.paths 即可获得查找路径

模块的加载过程

  1. 模块在被第一次引入时,模块中的js代码会被运行一次
// bar.js
console.log(`bar`)
// main.js
require("./bar")

console.log("main")

其打印顺序是先打印 bar 再打印 main

CommondJS 的加载是同步的,也就是说等到 require 加载的模块执行完毕之后,才会执行后续代码

CommondJS 的加载规则用在服务器上不会出现什么问题,因为文件都在本地,同步加载不会影响模块的执行

CommondJS 的加载规则如果用在浏览器中会出现大问题,必须等到 require 的文件下载完毕才能加载,会严重阻碍当前模块的运行

  1. 模块被多次引入时,会缓存,最终只加载(运行)一次

module 对象有一个属性叫做 loaded ,值为 false 表示没有被加载,为 true 表示被加载了

// bar.js
console.log(`bar`)
// foo.js
require("./bar")

console.log(`foo`)
// main.js
require("./bar")
require("./foo")

console.log(module.children)
console.log(module.loaded)

最终只会输出一个 bar,说明了一个模块只会被加载一次

关注一下 main.js 中的输出

  • module.children 输出了子模块的 module 信息,其中就包括其 loaded 的值
  • module.loaded 表示当前模块的是否被加载完毕,当前模块没有全部执行完毕,所以 loaded 的值是 false

module.children 中存储了所有加载的子模块的 module

  1. 如果存在循环引用,如何处理?

Node 采用的是深度优先算法,也就是 main => aaa => ccc => ddd => eee => bbb,按照这个顺序加载文件

// main.js

console.log("main")
require("./foo")
console.log("main finish")
// foo.js

console.log("foo")
require("./bar")
console.log("foo finish")
// bar.js

console.log("bar")
require("./main")
console.log("bar")

对应 node 代码

当前使用的 node 版本为 v16.13.2,不同版本目录可能不同

模块相关内容在 modules 文件夹中,其包含了两种加载规则 cjsesm

Module.prototype.require = function(id) {
  validateString(id, 'id');
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
};

Module 的原型上添加了 require 函数,其本质就是调用了 Module._load 函数

Module._load = function(request, parent, isMain) {
    if(parent) {
        // 如果存在父模块,说明当前模块被加载过,则直接从 Module._cache[filename] 中获取缓存的 module 对象
        return cachedModule.exports;
    }

    // ... to some thing
    return module.exports;
};