标签:index 例子 必须 common etc str 机制 顺序 了解
在最初 js 被设计用来做一些表单校验的简单功能,当初的 js 只是用来作为页面展示的一个补充。后来随着 web 的发展,相当一部分业务逻辑前置到了前端进行处理,js 的地位越来越重要,文件也越来越庞大,为了将大的功能模块进行拆分成一个一个小的组成部分,但是拆分成小的 js 文件又带来了新的挑战,由于 js 的加载和执行顺序在引入的时候就已经决定了,这样就需要充分考虑到各变量的作用范围以及各变量之间的依赖关系。
<html>
<body>
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>
</body>
</html>
就像上面这样,a.js
会最先被执行,这样如果在 b.js
中存在着与 a.js
同名的变量,就会发生覆盖。同时如果在 c.js
中有使用到 a.js
声明的变量就决定了 a.js
必须在 c.js
上面被引入。这样就存在这一种耦合,为了解决这一类问题 js 的模块化应运而生。
commonjs
随着 nodejs
的诞生而面世,主要是用来解决服务端模块化的问题,commonjs 对模块做了如下规定
exports
对象上的属性进行导出require
方法导入其他模块的变量node
中一个文件就是一个模块,各模块之间的变量是无法互相访问到的。
// a.js
const a = 1;
// b.js
const b = 2;
console.log(a); // ReferenceError: a is not defined
在 b.js
中无法访问到变量 a,如果需要使用 a 需要先导入模块
// b.js
const a = require(‘./a.js‘);
console.log(a) // {}
这里还是无法访问到 a 变量是因为模块 a 中没有导出对应的变量
// a.js
const a = 1
exports.a = a
// b.js
const a = require(‘./a.js‘);
console.log(a); // 1
module
是 node
中的一个内置对象,是 Module
类的一个实例, module
对象上有几个重要属性
module.id
模块的标识符。 通常是完全解析后的文件名
module.loaded
模块是否已经加载完成,或正在加载中
exports.x = 1;
console.log(module.loaded) // false 还没有加载完成
setTimeout(() => {
console.log(module.loaded) // true
}, 0)
module.exports
当前模块对外输出的接口,其他文件导入当前模块实际上就是在读取当前模块的 module.exports
变量
除了
module.exports
之外,node
中还提供了一个内置变量exports
,它是module.exports
的一个引用(可以理解成是一个快捷方式),看一下exports
和module.exports
的关系
/**
* 实际的引用关系
* module.exports = {}
* exports = module.exports
*/
/**
* 一
* 这样做的实际结果就是让
* module.exports = {x: 1}
*/
exports.x = 1; // {x: 1}
/**
* 二
* 同上
*/
module.exports.x = 1; // {x: 1}
/**
* 三
* 虽然最终导出的内容与上面两种做法是
* 一样的,但是这种做法改变了
* module.exports 的原始引用,导
* 致了 exports 与 module.exports 的
* 联系断掉了,如果再使用 exports.y = 2
* 是没有效果的
*/
module.exports = { x: 1 }; // {x: 1}
exports.y = 2; // 无效
/**
* 四
* 与上面类似,改变了 exports 的引用
*/
exports = {x: 1}; // 无效
module.exports.y = 2; // 2
require
是 node
模块中的内置方法,该方法用于导入模块,其函数签名如下:
interface require {
/**
* id 模块的名称或路径
*/
(id: string): any
}
require
方法上有几个比较重要的属性和方法
require.main
是 Module
的一个实例,表示当前 node
进程启动的入口模块
require.resolve
是一个方法,用来查询指定的模块的路径,如果存在会返回模块的路径(如果是原生模块,则只会返回原生模块的名称,例如 http
),不存在则会报出错误,与 require
不同的是这个方法只会查找对应的模块路径,不会执行模块中的代码,其函数签名如下
interface RequireResolve {
/**
* request 指定要查找的模块路径
* options.paths 从 paths 指定的路径中进行查找
*/
(request: string, options: {paths: string[]}): string
}
// /home/user/a.js
console.log(require.resolve(‘.b‘)); // /home/user/b.js
console.log(require.resolve(‘http‘)); // http
console.log(require.resolve(‘./index‘, {
paths: [‘/home/local/‘]
})); // /home/local/index.js
require.resolve
方法与require
解析文件路径的方式是一样的(后面会做介绍具体的解析过程),会优先查看是否是原生模块、然后会查看是否具有缓存、然后才是更具不同的文件扩展名进行查找
require.cache
是一个对象,被引入的模块将被缓存在这个对象中,可以手动进行删除
require
可以通过传入 string
类型的 id 作为入参,id 可以是一个文件路径或者是一个模块名称,路径可以是一个相对路径(以 ./ 或者 ../ 开头)或者是一个绝对路径(以 / 开头)。相对路径的方式比较简单,会以当前文件的 __dirname
作为基础路径计算出绝对路径,无论是相对路径还是绝对路径都可以是文件或者文件夹。
LOAD_AS_FILE(X)
LOAD_AS_FILE
1. 是否存在 X 文件,是则优先加载 X
2. 否则会加载 X.js
3. 否则会加载 X.json
4. 否则会加载 X.node
LOAD_AS_DIRECTORY(X)
LOAD_AS_DIRECTORY
1. 是否存在 `X/package.json`,是则继续
a. `package.json` 是否有 `main` 字段,无则执行 2,是则执行 b
b. 加载 `(X + main)` 文件,规则: `LOAD_AS_FILE(X + main)` ,无则继续执行 c
c. 加载 `(X + main)/index`,规则: `LOAD_AS_FILE((X + main)/index)`,无则抛出错误
2. 否则会执行去查找 `X/index`,规则: `LOAD_AS_FILE(X/index)`
id 作为模块名称会遵守如下优先级规则进行模块查找:
node_modules
文件夹中的模块node_modules
文件夹中的模块,一直到最顶层模块在第一次被加载之后会缓存,多次调用同一个模块只会让模块执行一次。
// a.js
module.exports = {
name: ‘张三‘
}
// b.js
require(‘./a.js‘) // {name: ‘张三‘}
require(‘./a.js‘).age = 18
require(‘./a.js‘) // {name: ‘张三‘, age: 18}
最后一个 require(‘./a.js‘)
会输出 {name: ‘张三‘, age: 18}
则说明 a.js
模块只执行了一次,返回的还是最早被缓存的对象。如果要强制重新执行被引用的模块代码,可以通过删除缓存的方式
// a.js
module.exports = {
name: ‘张三‘
}
// b.js
require(‘./a.js‘) // {name: ‘张三‘}
require(‘./a.js‘).age = 18
require(‘./a.js‘) // {name: ‘张三‘, age: 18}
delete require.cache[require.resolve(‘./a‘)]
require(‘./a.js‘) // {name: ‘张三‘}
上面的例子还能说明模块的缓存是基于文件路径进行的,只要在被加载时路径不一致同一个模块也会执行两次
要说弄清楚这个问题需要先了解 node 中模块加载机制,在 commonjs
模块体系中 require
加载的是一个对象的副本,实际也就是 module.exports
所指向的变量,所以除非是存在引用类型的变量否则模块内部的变化是影响不到外部的。举个例子说明这个:
// b.js
let count = 1
let countObj = {
count: 10
}
module.exports = {
count,
countObj,
setCount(newVal) {
count = newVal
},
setCountObj(newVal) {
countObj.count = newVal
}
}
// a.js
const moduleB = require(‘./b.js‘)
console.log(moduleB.count) // 1
moduleB.setCount(2)
console.log(moduleB.count) // 1
console.log(moduleB.countObj.count) // 10
moduleB.setCountObj(20)
console.log(moduleB.countObj.count) // 20
上面的例子说明了 require
的结果实际是 module.exports
的一个副本,按照这样的思路循环加载的情况下,也就会读取已经存在 module.exports
上的属性,如果还存在部分属性未挂在到 module.exports
上则会读取不到。
// a.js
console.log(‘a 开始‘);
exports.done = false;
const b = require(‘./b.js‘);
console.log(‘在 a 中,b.done = %j‘, b.done);
exports.done = true;
console.log(‘a 结束‘);
// b.js
console.log(‘b 开始‘);
exports.done = false;
const a = require(‘./a.js‘);
console.log(‘在 b 中,a.done = %j‘, a.done);
exports.done = true;
console.log(‘b 结束‘);
// main.js
console.log(‘main 开始‘);
const a = require(‘./a.js‘);
const b = require(‘./b.js‘);
console.log(‘在 main 中,a.done=%j,b.done=%j‘, a.done, b.done);
当 main.js
加载 a.js
时, a.js
又加载 b.js
。 此时, b.js
会尝试去加载 a.js
。 为了防止无限的循环,会返回一个 a.js
的 exports
对象的 未完成的副本 给 b.js
模块。 然后 b.js
完成加载,并将 exports
对象提供给 a.js
模块。
当 main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true
标签:index 例子 必须 common etc str 机制 顺序 了解
原文地址:https://www.cnblogs.com/kingsm/p/14226448.html