从 Nodejs 如何解决模块循环依赖问题来一点关于模块的发散思考

什么是模块循环依赖

所谓循环依赖就如字面理解的那样, 众所周知 Nodejs 对模块的解析是会提早加载到内存中, 当全部模块加载完才从入口文件开始 run 整个应用, 若是全部的模块之间都是按照顺序串行依赖, 就跟贪吃蛇同样是没什问题, 无非就是依赖链长一点, 不过要是贪吃蛇不当心咬到了身体或者尾巴, 就会行程环, 在代码中就比如前端

//a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

//b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

//main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
复制代码

这是一个 Nodejs 官网的示例, 正如示例中的那样, a.js 巴拉巴拉, 读到 require b.js, 而后跑去 b.js, b.js 巴拉巴拉 又读到 require a.js, 而后跑去 a.js, 这时候咱们不妨想一想第二次读取 a.js 的时候 Nodejs 该怎么处理呢, 若是像浏览器同样的话, 应该重头再解析一遍, 毕竟哪有半途而废的道理嘛, 因而从头再读...巴拉巴拉 又读到 require b.js 跑进去再重来, 等等这不是又回来了!?node

这就是模块循环依赖问题浏览器

要解决这个问题, 提及来也很简单, 只要把重来一遍变成中断就能够了, Nodejs 称为 unfinished copy, 第二进入 a.js 模块的时候, 从require b.js 的后面继续往下读取, 这样就将环解开又回到了原先串行的解析方式, 代价就是你得知道 require 先后都写了什么, 尤为是涉及 exports 出去的值, 由于执行顺序问题, 两次的值并不相同. 不过若是咱们不想中断, 就想从头至尾读呢?网络

那咱们就须要将模块解析和模块加载分开处理, 好比咱们 ES6 Module 带来的 import数据结构

//a.js
import b from 'b.js'
import c from 'c.js'

...coding...
//b.js
import a from 'a.js'

...coding...
复制代码

由于代码的执行分红了两个阶段, 意味着不管你 import 写在哪都会比其余代码先被读取, 这也就解决了 require 先后代码执行顺序致使 exports 值不一致的问题, 因此为啥说要 import 写在顶上, 那是为了符合直觉, 由于顺序就是从 import 先开始的, 因此从这个角度看若是 require 的设计撇开 module 这个概念, 好比 require 的不是 module 而是嗯片断, 就比较符合直觉, 由于是个超级大的 script , 可是若是加上 module 就有点反直觉, 由于咱们都会以为 module 和 module 之间是隔离的, 一个 module 的加载若是由于 require 致使被割裂...就感受这种设计好残疾, 与其说是 unfinished copy 不如说是 unfinished module. 因此用 copy 代替 module, 叫 CommonJS Copy 可能会更符合实际的设计.函数

回到正题, 经过解析加载分离, 咱们就能够先解析模块的依赖关系, 而避免去读取实际的模块代码, 在 a.js 中咱们读取到 import 'b.js', 这时候咱们能够给 b.js 添加一个 State, 并将其值设为 'Linked', 表示 b.js 已经被读取了, 而后从 b.js 读取到 import a.js, 咱们再将 a.js 的 State 设为 'Linked', 而后又回到 a.js, 发现 b.js 已是 Linked 了, 跳过, 继续读取 c.js , 经过状态标记就避免了循环依赖学习

Import 带来的礼物 Tree-Shaking

Tree-Shaking 是 rollup 提出的一个移除 dead code 的方案, 这里的摇树的概念应该是源自于 rollup 的具体处理方式, rollup 将代码转换成一颗 AST 而后摇啊摇, 其实就是不断的移除那些 unuesed 的变量/函数/类等等, 因此叫树摇...吧, 而之因此能这么作的前提其实就是基于 Import 的静态分析, 基于静态分析, 就能够先分析出 module 自己导出的对象, 和全部被其余 module 引入的对象, 找出那些导出未使用的变量, 而后再从 module 自身分析下是否有使用这些变量, 若是都没有, 那就摇掉吧...并且摇树背后其实意有所指, 好比当咱们使劲摇树的时候会掉什么!?测试

天然是 ---- 叶子...优化

在 AST 中掉下来的叶子就是那些 node 节点, 因此摇树, 或者树摇是否是很形象 😁ui

不过话说回来, 实现 Tree-Shaking 其实用到了两种基本的数据解构, 图和树, 因此学好数据结构仍是颇有必要的 😀 (说到这里, 我已经回去重修了... 😢)

CommonJS -> ES6 Module

Import 的设计比 require 更进一步, 不只解决了问题还引入可优化的方案, 在 Import 的基础上得以发展出 Tree-Shaking 这样的技术来进一步优化构建, 这是技术进步的意义所在, 并且 Import 还为远程模块留下了扩展支持, 像 Deno 那样 Import 一个远程模块, 若是发生在运行时那估计会比较糟糕, 若是加载的某个模块不可用, 会影响关联的全部模块, 对于大型应用而言这几乎是在灾难性的, 由于网络环境自己是不稳定的, 加上网络环境的不可知性, 会让你的模块系统陷入不可用的窘境, 但若是是静态分析, 咱们能够提早测试模块的可用性, 网络的介入多少会让问题变复杂, 而依赖分析和加载的解耦对于这种场景也可以很好的去支持, 因此 Import 基于静态分析的规范设计实际上是很是棒的, 像这种支持将来的设计都是很棒的设计!? (因此值得咱们好好学习, 陷入深深的思考)

后话

ES6 Module 是很棒的设计, 不过写这篇文章的时候难免稍稍有点伤感, 由于这类关于底层的标准设计大多数都是外国人, 嗯基本上是外国人, 在国内其实不多有碰到关于这类底层标准/规范设计的讨论, 好比 require 的实现或者说 CommonJS Module 的设计有缺陷, 但其实鲜有人关注, 或者有人关注了但不多引起讨论, 咱们彷佛已经习惯了接受某种嗯广为人知的设计或者接受某种已经存在的标准, 而不多独立思考, 甚至去质疑, 质疑为何这么设计

有时候我会以为前端圈像个投资圈, 咱们彷佛再不停的追逐一个又一个技术风口, 以致于诞生了有名的调侃 "学不动了" 其实这种调侃背后是咱们对不断追逐技术风口感到疲惫, 由于大多数人并无所以受益, 就好像投资圈赚钱的永远是那几个大佬, 普通老百姓都是韭菜, 不信? 你看看股市里的币圈里的韭菜们就知道了

或许咱们能够停下来, 思考一些东西, 质疑一些东西, 而不只仅是追逐.

相关文章
相关标签/搜索