目录html
1.什么是循环加载前端
“循环加载”简单来讲就是就是脚本之间的相互依赖,好比a.js
依赖b.js
,而b.js
又依赖a.js
。例如:node
// a.js const b = require('./b.js') // b.js const a = require('./a.js')
对于循环依赖,若是没有处理机制,则会形成递归循环,而递归循环是应该被避免的。而且在实际的项目开发中,咱们很难避免循环依赖的存在,好比颇有可能出现a
文件依赖b
文件,b
文件依赖c
文件,c
文件依赖a
文件这种情形。es6
也所以,对于循环依赖问题,其解决方案不是不要写循环依赖(没法避免),而是从模块化规范上提供相应的处理机制去识别循环依赖并作处理。api
接下来将介绍如今主流的两种模块化规范 CommonJS 模块和 ES6 模块是如何处理循环依赖以及它们有什么差别。缓存
2.CommonJS 模块的循环加载模块化
CommonJS 模块规范使用 require
语句导入模块,module.exports
语句导出模块。post
CommonJS 模块是运行时加载:ui
运行时遇到模块加载命令 require,就会去执行这个模块,输出一个对象(即
module.exports
属性),而后再从这个对象的属性上取值,输出的属性是一个值的拷贝,即一旦输出一个值,模块内部这个值发生了变化不会影响到已经输出的这个值。code
CommonJS 的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,而后在内存生成一个对象。对于同一个模块不管加载多少次,都只会在第一次加载时运行一次,以后再重复加载,就会直接返回第一次运行的结果(除非手动清除系统缓存)。
// module { id: '...', //模块名,惟一 exports: { ... }, //模块输出的各个接口 loaded: true, //模块的脚本是否执行完毕 ... }
上述代码是一个 Node 的模块对象,而用到这个模块时,就会从对象的 exports
属性中取值。
CommonJS 模块解决循环加载的策略就是:一旦某个模块被循环加载,就只输出已经执行的部分,没有执行的部分不输出。
用一个 Node 官方文档上的示例来说解其原理:
// 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);
main
脚本执行结果以下:
main
脚本 执行的顺序以下:
① 输出字符串 main starting
后,加载a
脚本
② 进入 a
脚本,a
脚本中输出的done
变量被设置为false
,随后输出字符串 a starting
,而后加载 b
脚本
③ 进入 b
脚本,随后输出字符串 b starting
,接着b
脚本中输出的done
变量被设置为false
,而后加载 a
脚本,发现了循环加载,此时不会再去执行a
脚本,只输出已经执行的部分(即输出a
脚本中的变量done
,此时其值为false
),随后输出字符串in b, a.done = false
,接着b
脚本中输出的done
变量被设置为true
,最后输出字符串 b done
,b
脚本执行完毕,回到以前的a
脚本
④ a
脚本继续从第4行开始执行,随后输出字符串in a, b.done = true
,接着a
脚本中输出的done
变量被设置为true
,最后输出字符串 a done
,a
脚本执行完毕,回到以前的main
脚本
⑤ main
脚本继续从第3行开始执行,加载b
脚本,发现b
脚本已经被加载了,将再也不执行,直接返回以前的结果,最终输出字符串in main, a.done = true, b.done = true
,至此main
脚本执行完毕
3.ES6 模块的循环加载
ES6 模块规范使用 import
语句导入模块中的变量,export
语句导出模块中的变量。
ES6 模块是编译时加载:
编译时遇到模块加载命令 import,不会去执行这个模块,只会输出一个只读引用,等到真的须要用到这个值时(即运行时),再经过这个引用到模块中取值。换句话说,模块内部这个值改变了,仍旧能够根据输出的引用获取到最新变化的值。
跟 CommonJS 模块同样,ES6 模块也不会再去执行重复加载的模块,而且解决循环加载的策略也同样:一旦某个模块被循环加载,就只输出已经执行的部分,没有执行的部分不输出。
但ES6 模块的循环加载与 CommonJS 存在本质上的不一样。因为 ES6 模块是动态引用,用 import
从一个模块加载变量,那个变量不会被缓存(是一个引用),因此只须要保证真正取值时可以取到值,即已经声明初始化,代码就能正常执行。
如下代码示例,是用 Node 来加载 ES6 模块,因此使用.mjs
后缀名。(从Node v13.2 版本开始,才默认打开了 ES6 模块支持)
实例一:
// a.mjs import { bar } from './b'; console.log('a.mjs'); console.log(bar); export let foo = 'foo'; // b.mjs import { foo } from './a'; console.log('b.mjs'); console.log(foo); export let bar = 'bar';
执行 a
脚本,会发现直接报错,以下图:
简单分析一下a
脚本执行过程:
① 开始执行a
脚本,加载b
脚本
② 进入b
脚本,加载a
脚本,发现了循环加载,此时不会再去执行a
脚本,只输出已经执行的部分,但此时a
脚本中的foo
变量还未被初始化,接着输出字符串a.mjs
,以后尝试输出foo
变量时,发现foo
变量还未被初始化,因此直接抛出异常
由于foo
变量是用let
关键字声明的变量,let
关键字在执行上下文的建立阶段,只会建立变量而不会被初始化(undefined),而且 ES6 规定了其初始化过程是在执行上下文的执行阶段(即直到它们的定义被执行时才初始化),使用未被初始化的变量将会报错。详细了解let
关键字,能够参考这篇文章深刻理解JS:var、let、const的异同。
实例二:用 var
代替 let
进行变量声明。
// a.mjs import { bar } from './b'; console.log('a.mjs'); console.log(bar); export var foo = 'foo'; // b.mjs import { foo } from './a'; console.log('b.mjs'); console.log(foo); export var bar = 'bar';
执行 a
脚本,将不会报错,其结果以下:
这是由于使用 var 声明的变量都会在执行上下文的建立阶段时做为变量对象的属性被建立并初始化(undefined),因此加载b
脚本时,a
脚本中的foo
变量虽然没有被赋值,但已经被初始化,因此不会报错,能够继续执行。
4.小结
ES6 模块与 CommonJS 模块都不会再去执行重复加载的模块,而且解决循环加载的策略也同样:一旦某个模块被循环加载,就只输出已经执行的部分,没有执行的部分不输出。但因为 CommonJS 模块是运行时加载而 ES6 模块是编译时加载,因此也存在一些不一样。
5.参考